iOSプログラミング with Swift 2


2016.07.14: created by

Swiftでゲームの盤面データを編集・保存・選択する

このページを読む前に、次の3つのページの内容を理解することをお勧めします。


  1. Xcode を起動して "Create a new Xcode project" で "Single View Application" として新しいプロジェクトを開きます。 ここではプロジェクト名を SwiftEditData としています。



  2. ViewController の新しいサブクラスを作ります。
  3. UIViewController のサブクラスとして3つのクラスを生成します。 それぞれのクラス名とファイル名は次のようにします。

    クラス名subclass of:ファイル名
    EditViewControllerUIViewControllerEditViewController.swift
    ListViewControllerUIViewControllerListViewController.swift
    SelectViewControllerUIViewControllerSelectViewController.swift

    project navigator のプロジェクト名の上でマウスを右ドラッグして "New File..." を選択します。 iOS の Source で Cocoa Touch Class を選んで Next をクリックします。 Class: には "EditViewController", Subclass of: には "UIViewController" Language: には "Swift" を選びます(クラス名は自由に選んで構いません)。 EditViewController.swift がプロジェクトに追加されます。













    これをもう2回繰り返して、合計で3つのファイルを追加します。




  4. Main.storyboard上の ViewController の右隣に、ウィンドウ右下の "Object library" から "View Controller" をドラッグして3個配置します。 さらに、新しいView Controller が選択されている状態でウィドウ右上の identity inspector からCustom class: に今作成した UIViewController のサブクラス を設定します。 また、Identity の Storyboard IDにも クラスと同じ名前をつけましょう(この名前は自由につけて構いませんが、説明を簡単にするためです)。
  5. これ以降、Main.storyboard 上の4個のUIViewController のサブクラスを それぞれ ViewController, EditViewController, ListViewController, SelectViewController と呼ぶことにします。







  6. Main.storyboard上の ViewController 上に Button を2個配置しButtonの表示を "Edit" と "List" にします。 "Edit" ボタンからは EditViewControllerに、 "List" ボタンから ListViewControllerに "Present Modally" でセグウェイを設定します。















  7. Storyboard上の EditViewController 上に Label 2個、TextField 2個、 Button 6個、ImageView 1個を配置します。 さらに、これらの UIView 達を EditViewController.swift 中のIBOutlet変数やIBAction関数に connect します。
  8. 種類connection変数名またはメソッド名
    "rows" Labelなし...
    TextFieldOutletrowsField変数
    Action (Editing Did End)rowsEditingEnd()関数
    "columns" Labelなし...
    UITextFieldOutletcolsField変数
    Action (Editing Did End)colsEditingEnd()関数
    "Back" ButtonAction (Touch Up Inside)tapBack()関数
    "Save" ButtonAction (Touch Up Inside)tapSave()関数
    "0" ButtonAction (Touch Up Inside)tapButton0()関数
    "1" ButtonAction (Touch Up Inside)tapButton1()関数
    "2" ButtonAction (Touch Up Inside)tapButton2()関数
    "3" ButtonAction (Touch Up Inside)tapButton3()関数
    UIImageViewOutletmyImageView変数






  9. ImageViewはデフォルトではタップを受け付けない設定になっています。EditViewController上の ImageView をタップを受け付ける設定に変更します。
  10. Attribute Inspector から Interaction の "User Interaction Enabled" と "Multiple Touch" にチェックを入れます




  11. さらに、Main.storyboard上で EditViewController の ImageView の上に向けて Tap Gesture Recognizer をドロップします。 EditViewControllerの上部に Tap Gesture Recognizer のアイコン が表示されたら正しく追加されています。



  12. EditViewControllerの上部に表示されている Tap Gesture Recognizer のアイコン から、 EditViewController.swift の中の tapImageView()関数に Action (引数のTypeは UITapGestureRecognizer を選択して) で connect します。









  13. EditViewController.swift を変更します。
  14. EditViewController.swiftに追加するコード(赤字部分)
    import UIKit
    
    class EditViewController: UIViewController {
        
        var rows: Int!
        var cols: Int!
        var mode: Int!
        var map: [Int]!
        
        let colors:[[CGFloat]] = [[ 0.7, 0.7, 0.7, 1.0 ],
                                  [ 1.0, 0.0, 0.0, 1.0 ],
                                  [ 0.0, 1.0, 0.0, 1.0 ],
                                  [ 0.0, 0.0, 1.0, 1.0 ]]
    
        @IBOutlet weak var rowsField: UITextField!
        @IBOutlet weak var colsField: UITextField!
        @IBOutlet weak var myImageView: UIImageView!
        @IBAction func rowsEditingEnd(sender: AnyObject) {
            if let t = Int(rowsField.text!) {
                changeMapSize(t,cols)
            }
            rowsField.text = String(rows)
        }
        @IBAction func colsEditingEnd(sender: AnyObject) {
            if let t = Int(colsField.text!) {
                changeMapSize(rows,t)
            }
            colsField.text = String(cols)
        }
        @IBAction func tapButton0(sender: AnyObject) {
            mode = 0
        }
        @IBAction func tapButton1(sender: AnyObject) {
            mode = 1
        }
        @IBAction func tapButton2(sender: AnyObject) {
            mode = 2
        }
        @IBAction func tapButton3(sender: AnyObject) {
            mode = 3
        }
        @IBAction func tapSave(sender: AnyObject) {
            let format = NSDateFormatter()
            format.dateFormat = "yyyy-MM-dd_HH-mm-ss"
            format.timeZone = NSTimeZone(abbreviation: "JST")
            let now = format.stringFromDate(NSDate())
            
            let path = NSHomeDirectory() + "/Documents/" + now + ".txt"
            var contents: String = "\(rows) \(cols)"
            for m in map {
                contents = contents + " " + String(m)
            }
            do {
                try contents.writeToFile(path, atomically:true, encoding:NSUTF8StringEncoding)
            } catch let error as NSError {
                let alert = UIAlertController(title:"Save", message: "error occurred: "+String(error), preferredStyle: UIAlertControllerStyle.Alert)
                alert.addAction(UIAlertAction(title:"Cancel", style:UIAlertActionStyle.Cancel,handler:nil))
                presentViewController(alert,animated:true,completion:nil)
            }
        }
        @IBAction func tapBack(sender: AnyObject) {
            dismissViewControllerAnimated(true, completion: nil)
        }
        @IBAction func tapImageView(sender: UITapGestureRecognizer) {
            let pos = sender.locationInView(myImageView)
            let sz:CGSize = myImageView.bounds.size
            let bw = sz.width / CGFloat(cols)
            let bh = sz.height / CGFloat(rows)
            let c = Int(pos.x / bw)
            let r = Int(pos.y / bh)
            map[r * cols + c] = mode
            drawMap()
        }
     
        func drawMap() {
            let sz:CGSize = myImageView.bounds.size
            let bw = sz.width / CGFloat(cols);
            let bh = sz.height / CGFloat(rows);
            UIGraphicsBeginImageContext(sz)
            let context: CGContextRef = UIGraphicsGetCurrentContext()!
            CGContextSetLineWidth(context, 2.0)
            CGContextSetRGBStrokeColor(context, 0.5, 0.5, 0.5, 1.0)
            for i in 0..<(rows*cols) {
                let r = CGFloat(i / cols)
                let c = CGFloat(i % cols)
                let m = map[i]
                CGContextSetRGBFillColor(context, colors[m][0], colors[m][1], colors[m][2], colors[m][3])
                let rect = CGRect(x: c * bw, y: r * bh, width: bw, height: bh)
                CGContextFillRect(context, rect)
                CGContextStrokeRect(context, rect)
            }
            myImageView.image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
        }
        
        func changeMapSize(rs:Int, _ cs:Int) {
            print("map = \(rows) x \(cols) to \(rs) x \(cs)")
            var map2 = Array<Int>(count: (rs*cs), repeatedValue: 0)
            for r in 0..<rs {
                for c in 0..<cs {
                    if (r < rows) && (c < cols) {
                        map2[r * cs + c] = map[r * cols + c]
                    }
                }
            }
            map = map2; rows = rs; cols = cs
            drawMap()
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            rows = rows ?? 8
            cols = cols ?? 8
            mode = mode ?? 0
            rowsField.text = String(rows)
            colsField.text = String(cols)
            map = Array<Int>(count: (rows*cols), repeatedValue: 0)
            drawMap()
       }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
        }
    
    }
    
  15. プログラムを実行してみましょう。
  16. 最初の画面で "Edit" ボタンを選択すると、マップ編集画面に遷移します。 rowsやcolumnsの横のテキストフィールドに数値を代入するとTabを押した瞬間、マップのサイズが変更されます。 数字のボタンをタップしてから、ImageViewをタップすると矩形領域の色が変ります。 Save ボタンでデータをファイルに保存します。ファイル名は"そのときの日時.txt" となります。


    --> -->


Swiftでゲームの盤面データを編集・保存・選択する(2)

ファイルの保存ができたので、作成したファイルの一覧を見たり、ファイルをロードする部分を作成します。

  1. Storyboard上の ListViewController 上に Button を1個配置し表示を "Back" にします。 また、Text Viewを1個配置します。 配置したUIView 達を ListViewController 中の変数や関数に connect します。
  2. 種類connection変数名またはメソッド名
    TableViewOutletmyTableView
    "Back" ButtonAction (Touch Up Inside)tapBack()関数






  3. ListViewController.swift を変更します。
  4. ListViewController.swiftに追加するコード(赤字部分)
    import UIKit
    
    class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
        var manager: NSFileManager!
        var fullPath: String!
        var paths: Array<String>!
    
        @IBOutlet weak var myTableView: UITableView!
        @IBAction func tapBack(sender: AnyObject) {
            dismissViewControllerAnimated(true, completion: nil)
        }
        
        func tableView(tableView: UITableView, numberOfRowsInSection section:Int) -> Int {
            if paths == nil {
                return 1
            }
            return paths.count;
        }
        
        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "Cell")
            cell.textLabel?.text = paths[indexPath.row]
            return cell
        }
        
        func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
            let storyboard = UIStoryboard(name:"Main",bundle:nil)
            let controller: SelectViewController = storyboard.instantiateViewControllerWithIdentifier("SelectViewController") as! SelectViewController
            self.presentViewController(controller, animated: true, completion: nil)
        }
        
        func refreshPaths() {
            paths = manager.subpathsAtPath(fullPath)
        }
        
        override func viewWillAppear(animated: Bool) {
            refreshPaths()
            self.myTableView.reloadData()
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            myTableView.delegate = self
            myTableView.dataSource = self
            manager = NSFileManager.defaultManager()
            fullPath = NSHomeDirectory() + "/Documents"
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
        }
    
    }
    
  5. プログラムを実行してみましょう。
  6. 最初の画面で "List" ボタンを選択すると、一覧表示画面に遷移します。 ここでは2つのファイルが生成されていることがわかります。 TableViewの項目をタップすると SelectViewController に遷移しますが、 そちらはまだ何も記述していないので戻る手段はありません。


    -->


Swiftでゲームの盤面データを編集・保存・選択する(3)

ファイル一覧から特定のファイルが選択されたとき、そのファイルを削除したり、 データをロードしたりできるようにします。

  1. Storyboard上の SelectViewController 上に Label を1個、Button を3個、Text Viewを1個配置し、 Buttonの表示を "Back", "Load", "Remove" にします。 さらに、SelectViewController.swift 中の変数や関数にconnectします。
  2. UIView オブジェクトconnection変数名またはメソッド名
    LabelOutletmyLabel変数
    Text ViewOutletmyTextView
    "Back" ButtonAction (Touch Up Inside)tapBack()関数
    "Load" ButtonAction (Touch Up Inside)tapLoad()関数
    "Remove" ButtonAction (Touch Up Inside)tapRemove()関数






  3. SelectViewController.swift を変更します。
  4. SelectViewController.swiftに追加するコード(赤字部分)
    import UIKit
    
    class SelectViewController: UIViewController {
    
        var path: String!     // this value should be set from the outer
        var fullPath: String!
    
        @IBOutlet weak var myLabel: UILabel!
        @IBOutlet weak var myTextView: UITextView!
        @IBAction func tapBack(sender: AnyObject) {
            dismissViewControllerAnimated(true, completion: nil)
        }
        @IBAction func tapLoad(sender: AnyObject) {
            if let txt = myTextView.text {
                let a:[String] = txt.componentsSeparatedByString(" ")
                if a.count > 2 {
                    let rows = Int(a[0])
                    let cols = Int(a[1])
                    if (rows != nil) && (cols != nil) {
                        var map = Array<Int>()
                        for s in a {
                            if let n = Int(s) {
                                map.append(n)
                            } else {
                                return
                            }
                        }
                        // success
                        let appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
                        appDelegate.rows = rows
                        appDelegate.cols = cols
                        appDelegate.map = map
                        print("\(rows) \(cols) \(map.count)")
                        dismissViewControllerAnimated(true, completion: nil)
                    }
                }
            }
        }
        @IBAction func tapRemove(sender: AnyObject) {
            do {
                try NSFileManager.defaultManager().removeItemAtPath(fullPath)
                dismissViewControllerAnimated(true, completion: nil)
            } catch let error as NSError {
                let alert: UIAlertController = UIAlertController(title:"Selected File",
                                                                 message: "error occurred: "+String(error),
                                                                 preferredStyle: UIAlertControllerStyle.Alert)
                alert.addAction(UIAlertAction(title:"Cancel",style:UIAlertActionStyle.Cancel,handler:nil))
                presentViewController(alert,animated:true, completion:nil)
            }
        }
        func fileContents() {
            let manager:NSFileManager = NSFileManager.defaultManager()
            var isDir: ObjCBool = false
            let flag = manager.fileExistsAtPath(fullPath, isDirectory:&isDir)
            if flag && Bool(isDir) {
                myTextView.text = "[[Directory]]"
            } else if flag {
                if fullPath.hasSuffix(".txt") {
                    do {
                        myTextView.text = try NSString(contentsOfFile: fullPath, encoding: NSUTF8StringEncoding) as String
                    } catch let error as NSError {
                        let alert: UIAlertController = UIAlertController(title:"Selected File",
                                                                         message: "cannot read .txt file: "+String(error),
                                                                         preferredStyle: UIAlertControllerStyle.Alert)
                        alert.addAction(UIAlertAction(title:"Cancel",style:UIAlertActionStyle.Cancel,handler:nil))
                        presentViewController(alert,animated:true, completion:nil)
                        
                    }
                } else {
                    myTextView.text = "[[not directory, but has no \".txt\" suffix]]"
                }
            } else {
                let alert: UIAlertController = UIAlertController(title:"Selected File",
                                                                 message: "No such file exists",
                                                                 preferredStyle: UIAlertControllerStyle.Alert)
                alert.addAction(UIAlertAction(title:"Cancel",style:UIAlertActionStyle.Cancel,handler:nil))
                presentViewController(alert,animated:true, completion:nil)
            }
        }
        
        func setup() {
            if path == nil {
                let alert: UIAlertController = UIAlertController(title:"Selected File",
                                                                 message: "path is nil: ",
                                                                 preferredStyle: UIAlertControllerStyle.Alert)
                alert.addAction(UIAlertAction(title:"Cancel",style:UIAlertActionStyle.Cancel,handler:nil))
                presentViewController(alert,animated:true, completion:nil)
                path = ""
            }
            fullPath = NSHomeDirectory() + "/Documents/" + path
            myLabel.text = path
        }
        
        override func viewDidAppear(animated: Bool) {
            setup()
            fileContents()
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
        }
        
    }
    
  5. ListViewController.swift を変更します。
  6. SelectViewController に遷移する前に、SelectViewController の path 変数に選択されたファイルの名前を代入するように変更します。 1行追加する必要があります(緑字の部分)。

    ListViewController.swiftに追加するコード(緑字部分)
    import UIKit
    
    class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
        var manager: NSFileManager!
        var fullPath: String!
        var paths: Array<String>!
    
        @IBOutlet weak var myTableView: UITableView!
        @IBAction func tapBack(sender: AnyObject) {
            dismissViewControllerAnimated(true, completion: nil)
        }
        
        func tableView(tableView: UITableView, numberOfRowsInSection section:Int) -> Int {
            if paths == nil {
                return 1
            }
            return paths.count;
        }
        
        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "Cell")
            cell.textLabel?.text = paths[indexPath.row]
            return cell
        }
        
        func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
            let storyboard = UIStoryboard(name:"Main",bundle:nil)
            let controller: SelectViewController = storyboard.instantiateViewControllerWithIdentifier("SelectViewController") as! SelectViewController
            controller.path = paths[indexPath.row]
            self.presentViewController(controller, animated: true, completion: nil)
        }
        
        func refreshPaths() {
            paths = manager.subpathsAtPath(fullPath)
        }
        
        override func viewWillAppear(animated: Bool) {
            refreshPaths()
            self.myTableView.reloadData()
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            myTableView.delegate = self
            myTableView.dataSource = self
            manager = NSFileManager.defaultManager()
            fullPath = NSHomeDirectory() + "/Documents"
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
        }
    
    }
    
  7. AppDelegate.swift を変更します。
  8. Loadされたデータは AppDelegateオブジェクトの変数 rows: Int!, cols: Int!, map:[Int]! に保存されています (この例では活用していません)。 ゲーム画面を追加して、そちらから AppDelegate にアクセスしてデータを利用することを想定しています。

    AddDelegate.swiftに追加するコード(赤字部分)
    import UIKit
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
        
        var rows: Int!
        var cols: Int!
        var map: [Int]!
    
        var window: UIWindow?
    
        func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
            return true
        }
        func applicationWillResignActive(application: UIApplication) {
        }
        func applicationDidEnterBackground(application: UIApplication) {
        }
        func applicationWillEnterForeground(application: UIApplication) {
        }
        func applicationDidBecomeActive(application: UIApplication) {
        }
        func applicationWillTerminate(application: UIApplication) {
        }
    }
    
  9. プログラムを実行します。
  10. まず最初の画面で List ボタンを選択し、 一覧からファイルを選択すると SelectViewController の画面に遷移します。 そちらで、ファイルからデータをロードしたり、ファイルを消去したりできます。

    簡単のため、 フォルダの詳細を表示した場合は、ファイルの内容は"[[Directory]]" とだけ表示します。 また、フォルダでない通常のファイルでも拡張子が".txt"でない場合は "[[not directory, but has no ".txt" suffix]]" と表示します。

    ファイルの詳細を表示中に "Remove" ボタンをタップすると、そのファイルやフォルダを消すことができます。 フォルダの中に他のファイルやフォルダが存在しても丸ごと消せるので注意すること。


    --> -->

  11. サンプルのプロジェクトはこちら。(Xcode 7.3.1版)


http://karel.tsuda.ac.jp