- 投稿日:2020-05-30T23:18:54+09:00
【プログラミング初心者】Swift基礎~値型・参照型~
はじめに
クラスと構造体の違いについて説明します。
Swift基礎~構造体・クラス~という記事で使った構造体やクラスを使って説明していくのでまずそちらを参照して頂ければと思います。今回の内容はC言語でいうところのポインタ、動的メモリ確保です。
今対象にしているSwiftなどの言語では意識することが少ない内容となります。
言い換えるとそこまで理解していなくてもSwiftならある程度実装できるので難しければ保留にしておいてもいいかもしれません。メモリについて
前提知識として少しメモリの話をします。
まずメモリを扱うには確保と開放という操作が必要になります。
そのままの意味ですが確保すると「今からこの領域を使います」というようにそのデータが対象のメモリを専有します。開放はその専有をやめることです。使用されているメモリは他から急に書き換えられると困るので開放されるまでは他からは使えません。よく一口にメモリと言っていますが、OSはこのメモリをスタック領域とヒープ領域に分けて管理しています。
スタック領域とヒープ領域は、どちらも一時的に確保されるメモリ領域ですがそれぞれ管理方法が異なります。
- スタック領域
- メモリがA→B→Cの順で確保された場合、C→B→Aの順で解放される。メモリをいわゆるスタックで管理している。
処理ブロック({}
で囲まれたブロック)を抜けると解放されるという特長がある。
ヒープよりも割り当てられている領域が小さい。- ヒープ領域
- 確保されたメモリは明示的に解放しないとずっと留まり続ける。そのため気をつけていなければメモリリークを引き起こしクラッシュの原因になる。
自由なサイズのメモリを確保できるというのが特長。変数の参照先は全てスタックに格納された値です。
ここまでは前提知識として覚えておいてください。クラスと構造体の違い
クラスと構造体は若干プログラムの定義の仕方が異なる部分もありますが基本的には同じように扱えます。
ですが根本的に異なる部分があります。
それは実体化したときのメモリの取られ方です。構造体、クラスはそれぞれ値型と参照型と呼ばれます。
値型
まず値型と呼ばれる構造体について。
Swift基礎~構造体・クラス~で作ったCompany
構造体を例にして説明していきます。
以前変数のメモリの取られ方について説明しました。
このときはIntを題材にしましたが、定義を見ればわかりますが実はIntも構造体です。
なのでメモリの長さは異なりますが、以前説明したメモリの取られ方と基本的には同じです。
詳細な取られ方はわかりませんが構造体は以下のようなイメージでメモリが確保されます。
値型、つまり構造体の場合値の全てがスタック領域に確保されます。
例えば以下のような宣言をした場合var company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000, departments: [])以下のようなイメージになります。
これは特に疑問なくイメージできるかと思います。今度は以下の場合を見てみましょう。
var company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000, departments: []) var tmpCompany = companyこの場合はどうでしょう?
これもスタックに確保されるのですが、新しく確保された領域にコピーされた値が格納されます。
値型の場合変数には値そのものが入っています。
そのため新しくtmpCompany
を宣言した場合もcompany
の値が代入されます。確認してみましょう。
以下のコードで出力を見てみます。var company: Company = Company(name: "株式会社山田商事", president: "山田太郎", earnings: 1000000, departments: [developDepartment, salesDepartment]) var tmpCompany = company company.name = "nameを更新" print("company: " + company.name) print("tmpCompany: " + tmpCompany.name)出力company: nameを更新 tmpCompany: 株式会社山田商事
tmpCompany.name
が変更されていないことがわかりますね。
つまり別の領域として確保されたということです。値型の場合スタックに値がコピーされ続けるのでデータ量の多い構造体の場合メモリを大きく食いつぶす可能性があります。
スタック領域はヒープ領域に比べて容量が小さいのですぐ使い切ってしまいます。
この場合スタックを使い切ってしまったエラーをスタックオーバーフローといいます。
プログラムは当然クラッシュします。参照型
一方参照型と呼ばれるクラスはヒープ領域に確保されます。
変数は全てスタック領域に確保された値と先述しました。
では参照型の変数にはどんな値が確保されるのでしょうか?答えはヒープに確保された領域の先頭アドレスです。
Swift基礎~構造体・クラス~作った
Human
クラスを例にして説明します。以下のコードでオブジェクトを生成し変数に格納します。
var human = Human(name: "山田太郎", age: 25)クラスの場合ヒープ領域に確保されると言いましたが、その確保のされ方は値型がスタックに確保される時と同じです。
違うのはHuman(name: "山田太郎", age: 25)
が返す値が生成された領域の先頭アドレスということだけです。
つまりhuman
にはオブジェクトのアドレスが格納されています。
図で表すと以下の様になります。
このように値そのものではなく、オブジェクトへの参照を変数に格納することから参照型と呼ばれています。
(C言語ではポインタ型と言われ、この場合Humanポインタ型と呼びます)では以下のように
human
を新しい変数に格納した場合はどのようになるでしょう?var human = Human(name: "山田太郎", age: 25) var tmpHuman = human何度か言っているように変数はスタックに確保されます。
そして値型どうように変数宣言をすると新しくスタック領域に値のコピーが作成されます。
つまり以下のようになります。
このように変数にはアドレスという値がコピーされ、実際指し示すアドレスは同じオブジェクトなのでhuman
とtmpHuman
は同じものと見なすことができます。確認してみましょう。
以下のコードで出力を見てみます。var human = Human(name: "山田太郎", age: 25) var tmpHuman = human human.name = "nameを更新" print("human: " + human.name) print("tmpHuman: " + tmpHuman.name)出力human: nameを更新 tmpHuman: nameを更新
human
を更新するとtmpHuman
も一緒に更新されました。
つまり同一オブジェクトを参照していることがわかりますね。このように参照型の場合は変数に代入するときアドレスのみがコピーされます。
これを浅いコピーという意味でシャローコピー(shallow copy)と言います。
反対に値型の場合内部のメンバ変数全てが別領域に同じ値としてコピーされるので、深いコピーディープコピー(deep copy)と呼びます。自分がやっている実装はshallow copyなのかdeep copyなのかをしっかり意識しておかなければ、いつの間にか書き換わっていたり書き換えたと思っていたのに書き換わっていなかったということになりかねません。
気をつけて実装してください。ガベージコレクション
少し話が変わります。
冒頭のヒープ領域の説明で以下のように説明しました。確保されたメモリは明示的に解放しないとずっと留まり続ける。
C言語の場合不要になった時
free(変数)
というように明示的に開放処理を行います。Swiftではどうやって解放すればいいのでしょうか?
結論から言うとSwiftでは解放する必要がありません。
というよりはシステム的に自動的に解放されるようになっています。この自動で解放してくれる仕組みのことをガベージコレクション(garbage collection:GC)と呼びます。
GCではヒープで確保した領域に対して参照カウンタというものを持ちます。
そのままの意味で何個の変数から参照されているのかカウントするものです。例えば先程使った例でいうと
var human = Human(name: "山田太郎", age: 25) var tmpHuman = humanというように2つの変数から参照しました。
この段階で参照カウンタは2となります。ここから
tmpHuman = nilとします。
nil
はどこのアドレスも指していないという意味です。
そうすると参照カウンタが-1され1となります。
この参照カウンタが0になったタイミングでオブジェクトは解放されます。これがGCという機能です。
また
tmpHuman = nil
のようにしなくても問題ありません。
変数は全てスタック、スタックは処理ブロックを抜けると解放されると説明しました。つまり
{}
を抜けると変数は解放され、参照カウントが自動的に-1されるというわけです。
そのためSwiftではあまり解放を意識する機会はありません。ITの基本知識として押さえる程度でいいかと思います。
最後に
今回クラスと構造体の違いについて説明しました。
このあたりは知らないと思わぬバグを引き起こす原因になりうるので知っていてほしい内容ではあります。
配列などは参照型なのか値型なのか言語によって分かれるところで、新しい言語を触るとよくバグる部分だったりします。ですが意外とプログラマの中にもあまりしっかりと理解せずコーディングしている人も多いように感じます。
最近の言語は便利になったため知らなくてもコーディングできてしまうためかと思います。
なので最初のうちはわからなくても問題ないかと思います。ですがいずれ身につけてもらえるとプログラムを深く理解できるようになるかと思います。
今回の内容は以上です。
本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684
- 投稿日:2020-05-30T23:00:59+09:00
Swift Package Managerで簡単なコマンドラインツールを作って試す
Swift Package Manager
環境
$ swift package --version Swift Package Manager - Swift 5.2.0今回は新たにコマンドラインツールを作る想定で環境を作っていきたいと思います。
$ mkdir spm-sample $ cd spm-sample $ swift package init --type=executable Creating executable package: spm-sample Creating Package.swift Creating README.md Creating .gitignore Creating Sources/ Creating Sources/spm-sample/main.swift Creating Tests/ Creating Tests/LinuxMain.swift Creating Tests/spm-sampleTests/ Creating Tests/spm-sampleTests/spm_sampleTests.swift Creating Tests/spm-sampleTests/XCTestManifests.swift
--type
に関しては以下の種類があります。
- empty
- library
- system-module
- executable
- manifest一旦ビルドして実行してみます。
$ swift build $ ./.build/debug/spm-sample Hello, world!ちょっとしたツールの作成
作るもの
折角なので、iOS開発を行っていたらいつの間にか色んなファイルで
容量を食ってしまうものを一斉に削除するツールを作ってみたいと思います。
プロジェクト名はiCleaner
として作成します。必要なパッケージを導入
↓のパスを簡単に扱えるようにする
PathKit
を導入したいと思います。
Package.swift
に以下を追加import PackageDescription let package = Package( name: "iCleaner", dependencies: [ .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.0") // ☆追加 ], targets: [ .target( name: "iCleaner", dependencies: ["PathKit"]), // ☆追加 .testTarget( name: "iCleanerTests", dependencies: ["iCleaner"]), ] )試しに以下を
main.swift
に書いて実行してみます。import Foundation import PathKit let path = FileManager.default.currentDirectoryPath let currentPath = Path(path) print(currentPath.glob("*.swift"))$ swift run [XXX/iCleaner/Package.swift]そしてツールの完成形はこちら
Cleaner.swiftimport Foundation import PathKit class Cleaner { static let DEVICE_SUPPORT_PATH = "Library/Developer/Xcode/iOS DeviceSupport" static let DEVICE_LOGS_PATH = "Library/Developer/Xcode/iOS Device Logs" static let ARCHIVES_PATH = "Library/Developer/Xcode/Archives" static let DERIVEDDATA_PATH = "Library/Developer/Xcode/DerivedData" let homePath: Path init() { let path = NSHomeDirectory() homePath = Path(path) print("HOME: \(homePath)") } func run() { // Device support let deviceSupportPath = homePath + Path(Cleaner.DEVICE_SUPPORT_PATH) if deviceSupportPath.exists { deviceSupportPath.glob("*").forEach { try? $0.delete() } } // Device logs let devicelogsPath = homePath + Path(Cleaner.DEVICE_LOGS_PATH) if devicelogsPath.exists { devicelogsPath.glob("*.db").forEach { try? $0.delete() } devicelogsPath.glob("*.db-shm").forEach { try? $0.delete() } devicelogsPath.glob("*.db-wal").forEach { try? $0.delete() } } // Archives let archivesPath = homePath + Path(Cleaner.ARCHIVES_PATH) if archivesPath.exists { archivesPath.glob("*").forEach { try? $0.delete() } } // DerivedData let derivedDataPath = homePath + Path(Cleaner.DERIVEDDATA_PATH) if derivedDataPath.exists { derivedDataPath.glob("*").forEach { try? $0.delete() } } } }main.swiftlet cleaner = Cleaner() cleaner.run()
swift run
で実行する事ができます。オプションを指定できるようにする
Device Support
に関しては使用中のものもあると思うので試しにオプションでスキップできるように
してみたいと思います。PathKitと同じ作者の
Commander
を使用します。
Package.swift
にCommander
を追加let package = Package( name: "iCleaner", dependencies: [ .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.0"), .package(url: "https://github.com/kylef/Commander.git", from: "0.9.1") // 追加 ], targets: [ .target( name: "iCleaner", dependencies: ["PathKit", "Commander"]), // Commander追加 .testTarget( name: "iCleanerTests", dependencies: ["iCleaner"]), ] )
main.swift
をオプションが受け付けれるように修正main.swiftimport Commander command( Option("skipds", default: 1, description: "Skip delete Device Support file's.(0: no skip/1: skip)") ) { skipds in let cleaner = Cleaner() cleaner.run(skipDeviceSupport: skipds == 1) }.run()
Cleaner
クラスにスキップフラグを追加func run(skipDeviceSupport: Bool) { if !skipDeviceSupport { // Device support let deviceSupportPath = homePath + Path(Cleaner.DEVICE_SUPPORT_PATH) if deviceSupportPath.exists { deviceSupportPath.glob("*").forEach { try? $0.delete() } } } ... }リリースビルドして確認してみます。
$ swift build -c release $ ./.build/release/iCleaner --help Usage: $ ./.build/release/iCleaner Options: --skipds [default: 1] - Skip delete Device Support file's.(0: no skip/1: skip) $ ./.build/release/iCleaner --skipds 0 #=> スキップされずに実行うまくスキップのON/OFFが切り替えれたらOKです。✨
参考にさせて頂いたURL
- 投稿日:2020-05-30T22:37:41+09:00
NeumorphismなUIButton
※ gifは、影の大きさを調節する関数を実装する前のものです。
はじめに
NeumorphismなUIButtonを作ったときのことを記録に残しておきます。
このButtonのしくみ
見てもらうとすぐわかるんですが、
クリックされていないときは、このボタンは
- ボタンの外側の上と左に光があたっている。
- ボタンの外側の下と右に影がついている。
というような特徴をもっています。
それだけでなく、
- 押されているときはボタンの内側の上と左に影がついている。
- 押されているときはボタンの内側の下と右に光があたっている。
というような特徴をもっています。
この計4つの特徴さえわかってしまえば、あとはそのとおりにUIButtonをいじっていくだけです。
実際のコード
実際のコードを公開します。
- Colors.swift
- PlainSquareButton.swift
という2つのファイルを使って実装しました。
Colors.swiftimport Foundation import UIKit class Colors { static var plainColor = UIColor(hex: "ECF0F3") } extension UIColor { convenience init(hex: String, alpha: CGFloat = 1.0) { let v = Int("000000" + hex, radix: 16) ?? 0 let r = CGFloat(v / Int(powf(256, 2)) % 256) / 255 let g = CGFloat(v / Int(powf(256, 1)) % 256) / 255 let b = CGFloat(v / Int(powf(256, 0)) % 256) / 255 self.init(red: r, green: g, blue: b, alpha: min(max(alpha, 0), 1)) } func brighter() -> UIColor { var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { return UIColor(hue: hue, saturation: saturation * 0.85, brightness: brightness * 1.1, alpha: alpha) } else { return self } } func darker() -> UIColor { var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { return UIColor(hue: hue, saturation: saturation * 1.25, brightness: brightness * 0.75, alpha: alpha) } else { return self } } }PlainSquareButton.swiftimport Foundation import UIKit class PlainSquareButton: UIButton { private let highlightLayer = CALayer(), shadowLayer = CALayer() private let dentedHorizontalLayer = CAGradientLayer(), dentedVerticalLayer = CAGradientLayer() required init?(coder: NSCoder) { super.init(coder: coder) self.layer.cornerRadius = 5.0 self.backgroundColor = Colors.plainColor self.putHighlight() self.putShadow() self.addTarget(self, action: #selector(self.onPushed), for: .touchDown) self.addTarget(self, action: #selector(self.onReleased), for: .touchUpInside) } private func putHighlight() { self.highlightLayer.masksToBounds = false self.highlightLayer.frame = self.bounds self.highlightLayer.backgroundColor = Colors.plainColor.cgColor self.highlightLayer.shadowColor = Colors.plainColor.brighter().cgColor self.highlightLayer.cornerRadius = 5.0 self.highlightLayer.shadowOpacity = 0.75 self.highlightLayer.shadowOffset = CGSize(width: -6, height: -6) self.highlightLayer.shadowRadius = 5.0 self.layer.addSublayer(self.highlightLayer) } private func putShadow() { self.shadowLayer.masksToBounds = false self.shadowLayer.frame = self.bounds self.shadowLayer.backgroundColor = Colors.plainColor.cgColor self.shadowLayer.shadowColor = Colors.plainColor.darker().cgColor self.shadowLayer.cornerRadius = 5.0 self.shadowLayer.shadowOpacity = 0.65 self.shadowLayer.shadowOffset = CGSize(width: 6, height: 6) self.shadowLayer.shadowRadius = 5.0 self.layer.addSublayer(self.shadowLayer) } @objc dynamic func onPushed() { self.highlightLayer.removeFromSuperlayer() self.shadowLayer.removeFromSuperlayer() self.putDentedVerticalLayer() self.putDentedHorizontalLayer() } private func putDentedVerticalLayer() { self.dentedVerticalLayer.cornerRadius = 5.0 self.dentedVerticalLayer.frame = self.bounds self.dentedVerticalLayer.colors = [ Colors.plainColor.darker().cgColor, Colors.plainColor.cgColor, Colors.plainColor.cgColor, Colors.plainColor.brighter().cgColor ] self.dentedVerticalLayer.locations = [ 0, 0.15, 0.85, 1 ] self.dentedVerticalLayer.opacity = 1 self.layer.insertSublayer(self.dentedVerticalLayer, at: 0) } private func putDentedHorizontalLayer() { self.dentedHorizontalLayer.cornerRadius = 5.0 self.dentedHorizontalLayer.frame = self.bounds self.dentedHorizontalLayer.colors = [ Colors.plainColor.darker().cgColor, Colors.plainColor.cgColor, Colors.plainColor.cgColor, Colors.plainColor.brighter().cgColor ] self.dentedHorizontalLayer.locations = self.getProperWidthLocations(size: self.bounds.size) self.dentedHorizontalLayer.startPoint = CGPoint(x: 0, y: 0) self.dentedHorizontalLayer.endPoint = CGPoint(x: 1, y: 0) self.dentedHorizontalLayer.opacity = 0.5 self.layer.insertSublayer(self.dentedHorizontalLayer, at: 1) } @objc dynamic func onReleased() { self.dentedVerticalLayer.removeFromSuperlayer() self.dentedHorizontalLayer.removeFromSuperlayer() self.putHighlight() self.putShadow() } private func getProperWidthLocations(size: CGSize) -> [NSNumber] { if (size.width >= size.height*2 && size.width < size.height*3) { return [ 0, 0.075, 0.925, 1 ] } else if (size.width >= size.height*3 && size.width < size.height*4) { return [ 0, 0.05, 0.95, 1 ] } else if (size.width >= size.height*4 && size.width < size.height*5) { return [ 0, 0.0325, 0.9675, 1 ] } else { return [ 0, 0.15, 0.85, 1 ] } } private func getProperHeightLocations(size: CGSize) -> [NSNumber] { if (size.width*2 >= size.height && size.width*3 < size.height) { return [ 0, 0.075, 0.925, 1 ] } else if (size.width*3 >= size.height && size.width*4 < size.height) { return [ 0, 0.05, 0.95, 1 ] } else if (size.width*4 >= size.height && size.width*5 < size.height) { return [ 0, 0.0325, 0.9675, 1 ] } else { return [ 0, 0.15, 0.85, 1 ] } } }解説
まず、イニシャライザで
putHighlight()
というメソッドと、putShadow()
というメソッドを呼び出します。それぞれのメソッドで光と影をボタンの外側につけます。
putHighlight()
では、plainColor
を少し明るくしたものを、ボタンから(-6, -6)くらいの位置まで届くように表示しています。
逆にputShadow()
では、plainColor
を少し暗くしたものを、(6, 6)くらいの位置まで届くように表示しています。
すると、とりあえずこの画像の状態が完成します。凹ませた状態を作るのがちょっと厄介で、
まずCAGradientLayer
を使って、始点の方に光、終点の方に影があたるようにグラデーションを設定します。
これを縦と横につけるのですが、locations
の数値を一緒にしていると、影の長さが縦と横で合わなくなってしまうので、ざっくりと合うようにgetProperHeightLocations()
のようなメソッドを用意しておきました。
そして作成されたレイヤを貼り付けることで、凹んだ状態を再現できます。
表示されていると困る方のレイヤがremoveしてしまいましょう。これで、gifのようなボタンが再現できると思います。
おわり
けっこう楽しいので、この調子で色々な部品を作ってみようと思います。
- 投稿日:2020-05-30T20:36:41+09:00
【iOS】iOSアプリ開発入門~ 画面遷移編3~
はじめに
前回はiOSアプリにおける画面遷移の種類についてお話しました。
画面遷移編2:https://qiita.com/euJcIKfcqwnzDui/items/6d37aaf00c0bc7ce26ca今回はその具体的な実装方法について説明していきます。
以前モーダル遷移の実装をしたプロジェクトをベースとして説明していきます。前準備
その前に、今までプロジェクト作成されたときに生成された
ViewController.swift
とMain.storyboard
をそのまま使ってきましたが、画面が多くなるとわかりにくくなるので変更しておきましょう。
それぞれFirstViewController.swift
とFirstViewController.storyboard
にリネームします。まずはファイル名をリネームします。
ファイルを選択しもう一度ファイル名をクリックするとリネームできます。
次はクラス名を変更します。
FirstViewController.swiftimport UIKit class FirstViewController: UIViewController { // ViewControllerから変更 /// テキストフィールド @IBOutlet weak var textField: UITextField! /// 画面遷移ボタンタップ処理 /// - Parameter sender: ボタン @IBAction func didTapTransitionButton(_ sender: Any) { ... ...クラス名を変更すると今までそのクラスを使用していた場所も変更してあげないといけません。
今の状態だとFirstViewController.storyboard
のViewController
オブジェクトに設定されています。
FirstViewController.storyboard
を開きViewController
オブジェクトを選択します。
その状態で[Identity Inspector]を選択し、[class]の項目をFirstViewController
と入力します。
ここで一度シミュレータで実行してみましょう。
すると、起動直後アプリがクラッシュし以下のエラーが表示されます。Thread 1: Exception: "Could not find a storyboard named 'Main' in bundle NSBundle ...「Mainという名前のstoryboardが見つからない」とのこと。
Main.storyboard
もリネームしているので当然です。
アプリを起動した後どのstoryboardから表示するのかXcodeに教えてあげなければいけません。
プロジェクトを作成した段階ではMain.storyboard
が指定されています。
これをFirstViewController.storyboard
に変更してあげます。フォルダ構成の一番上、プロジェクト名を選択します。
ここではプロジェクトの全体的な設定をすることができます。
[TARGETS]にはいくつかありますが一番上のプロジェクト名を選択します。
さらに上のタブで[General]を選択。
[Development Info]に[Main Interface]という項目があるのでプルダウンからFirstViewController.storyboard
を選択してください。
さらにXcode11からInfo.plist
というファイルにも「Main」が設定されています。
その中の[Storyboard Name]の項目を「FirstViewController」と書き換えてください。
これでファイル名の変更は完了です。
シミュレータで実行し問題ないか確認してください。モーダル
これは以前説明したので省きます。
https://qiita.com/euJcIKfcqwnzDui/items/679b1cd30694519f4916#%E7%94%BB%E9%9D%A2%E9%81%B7%E7%A7%BB%E3%81%99%E3%82%8Bプッシュ
segueを使った実装
プッシュの実装方法について説明します。
プッシュの画面遷移には以前説明したsegueを使った実装をします。まずは
Main.storyboard
を開きます。
プッシュの遷移にはUINavigationController
というクラスを使用します。
このUINavigationController
をStoryboardに追加しましょう。
FirstViewController
を選択します。
その状態でXcode上部のツールバーから[Edior]>[Embed In]>[Navigation Controller]を選択していってください。
FirstViewController
にくっつくような形でNavigation Controller
が追加されました。
Navigation Controller
は画面のような見た目をしていますが、これはあくまでStoryboard上の表示での話であって実際はユーザから見られる画面というわけではありません。
UINavigationController
はControllerとあるようにコントロールするためのクラスです。
Navigationは画面遷移と言い換えてもらえればいいかと思います。
つまりUINavigationController
は画面遷移を制御するためのクラスであって画面そのものではありません。
今のNavigation Controller
を追加した操作はUINavigationController
という概念のようなオブジェクトをFirstViewController
に追加するというイメージです。もう少し補足すると
UIViewController
もControllerとあるように、これ自体が画面というわけではありません。
これも画面をコントロールするオブジェクトで、実際に表示されている画面はFirstViewController
の中にあるView
というUIです。
コントローラは画面の表示制御やユーザからのイベントを扱うという役割です。少し細かい話をしてしまいました。
実装を進めます。
FirstViewController
にあるsegueを選択してください。
その状態で[Attributes Inspector]を選択すると[Kind]という項目があるので選択します。
ポップアップメニューが表示されるので[Show(e.g. Push)]を選択します。
これでsegueがプッシュ遷移用に変更されました。
Storyboardの設定はこれだけです。あとはソースコードから処理実行するだけです。
ですがこれはモーダル遷移の場合と同じでperformSegue
を呼び出してあげるだけです。
segueのidentifierを変えていないので以前モーダルを実装したままのコードですでにプッシュ遷移ができます。
該当のコードは以下です。FirstViewController.swiftself.performSegue(withIdentifier: "SecondViewController", sender: nil)実装は以上です。
シミュレータで実行し動作を確認してみてください。またパラメータもモーダルと同様の方法で渡すことができます。
FirstViewController.swiftlet text = textField.text self.performSegue(withIdentifier: "SecondViewController", sender: text)UINavigationControllerについて
UINavigationController
では一連のプッシュ遷移における画面を管理します。
UINavigationController
はrootViewController
というプロパティを持ち、プッシュ遷移の一番最初の画面を保持します。
Storyboardから追加した今回の場合だと、UINavigationController
を追加したFirstViewController
がrootViewController
になります。また
UINavigationController
はviewControllers: [UIViewController]
というプロパティを持っており、遷移した画面を配列で管理しています。
今回の例では画面遷移が完了した状態ではviewControllers[0]
にFirstViewController
、viewControllers[1]
にSecondViewController
が格納されています。プッシュ遷移先の
SecondViewController
に[戻る]ボタンが表示されていたかと思います。
[戻る]ボタンをタップすると一画面前に戻ります。
画面を戻るとSecondViewController
はUINavigationController
の管理対象からはずれ解放されます。
つまりviewControllers[1]
は存在しなくなります。
図で表すと以下のようになります。
SecondViewControllerからさらに遷移する
当然
SecondViewController
からThirdViewController
という画面にさらにプッシュ遷移した場合管理下に追加されviewControllers[2]
に格納されます。実装してみます。
これは特に難しい話ではなく同様にSecondViewController
からThirdViewController
へのsegueを作ってあげてperformSegue
を呼び出してあげるだけです。
SecondViewController
を追加した手順と同様にThirdViewController.swift
、ThirdViewController.storyboard
を追加以下のように編集してください。ThirdViewController.swiftimport UIKit class ThirdViewController: UIViewController { }さらに
SecondViewController.storyboard
からThirdViewController
へのsegueをプッシュで作成してください。
またSecondViewController.storyboard
にボタンを追加し、ボタンタップでThirdViewController
に遷移するようにします。
シミュレータで実行すると3画面遷移できるようになります。
SecondViewController/// 画面遷移ボタンタップ処理 /// - Parameter sender: ボタン @IBAction func didTapTransitionButton(_ sender: Any) { // 画面遷移 self.performSegue(withIdentifier: "ThirdViewController", sender: nil) }ここで注意してほしいことは
SecondViewController.storyboard
にUINavigationController
は追加しないということです。
UINavigationController
はrootとなる画面にのみ追加します。
今rootはFirstViewController
としているためSecondViewController
はrootではありません。
SecondViewController
からプッシュ遷移する場合、rootに設定したUINavigationController
が参照されます。予想がつくかと思いますが
ThirdViewController
で[戻る]ボタンを押すと以下のような構成になります。
このようにUINavigationController
では最後に追加されたものから消えていきます。
この管理方法をスタック(stack)と呼びます。
余談ですがスタックは画面管理の方法ではなく配列の管理方法の1つです。
スタックは基本情報試験にも出てくるIT技術の基礎知識なので抑えておきましょう。
別の配列管理方法ではキュー(queue)があります。一度調べておいてください。UINavigationControllerの参照のされ方
ここまでの画面遷移処理で少し違和感を覚える方もいるかもしれません。
rootである
FirstViewController
はStoryboardでUINavigationController
のオブジェクトを追加したので画面遷移にUINavigationController
を使えるのはなんとなくわかるかと思います。ですが
SecondViewController
にはUINavigationController
は設定されていないはずなのにどうして使えるのでしょうか?実は
UIViewController
にはnavigationController
というように現在自分が管理されているUINavigationController
をプロパティとして持っています。
画面遷移の具体的な処理はUIKit
が勝手にやってくれているので詳細は不明なのですが、おそらくプッシュ遷移のタイミングで自分のnavigationController
を次の画面に一緒にセットしています。従って1つの
UINavigationController
のオブジェクトが次へ次へと共有されているのです。
図にすると以下のようなイメージになります。
このような形でroot画面からプッシュ遷移した画面の全てを管理しています。タブ
segueを使った実装
タブ遷移の実装方法について説明します。
タブに関してもsegueを使った実装をします。タブの遷移には
UITabBarController
というクラスを使用します。
UITabBarController
を扱うために、まずTabViewController.storyboard
というファイルを追加しましょう。
TabViewController.storyboard
を開き、[+]ボタンからUITabBarController
を探しStoryboardに追加します。
Tab Bar Controller
というオブジェクトと一緒にitem 1 Scene
とitem 2 Scene
というオブジェクトが追加されました。
これはタブTab Bar Controller
がデフォルトで表示する画面テンプレートです。
今回は不要なので削除します。
それぞれ選択し削除しておいてください。
Tab Bar Controller
も画面のような形ですが、これも「Controller」とあるように画面そのものではなくタブ遷移において画面を管理するオブジェクトです。
階層1つしたにあるTab Bar
というオブジェクトは画面一番下に表示されるタブ自体です。これは表示されるUIでUITabBar
というクラスです。この
TabBarController.storyboard
で最初に参照するオブジェクトはTabBarController
なので[Is Initial View Controller]のチェックを付けます。
TabBarController
が画面を追加します。
FirstViewController.storyboard
、SecondViewController.storyboard
、ThirdViewController.storyboard
への参照を追加します。
他の画面遷移と同様Storyboard Reference
を画面数だけ配置します。
配置したStoryboard Reference
にそれぞれStoryboardを設定します。
TabBarController
からそれぞれのStoryboard Reference
までsegueを作成します。
segueの種類は[Relationship Segue]の[view controllers]を選択してください。
以下のようになればタブ遷移の準備は完了です。
次はアプリ起動直後に表示するStoryboardを変更します。
先述したMain.storyboard
をリネームする際、説明したようにプロジェクトの[Main Interface]とInfo.plist
の[Storyboard Name]を「TabBarController」に変更してください。
シミュレータで実行するとタブバーが表示されます。
タブバーには何も表示されていませんが、タブボタン自体は配置されています。
適当な場所をタップするとタブ遷移されます。
ですがこれではユーザからすると何のUIかわからないので画像と名前を設定してあげましょう。
SecondViewController.storyboard
を開いてください。
[+]ボタンからUITabBarItem
を探し、SecondViewController
に追加してください。
UITabBarItem
はタブボタンのオブジェクトです。
ここにアイテム名や画像を設定してあげるとそれがタブに表示されます。
UITabBarItem
を選択し、[Attributes Inspector]の[BarItem]に[Title]と[Image]を設定してください。
※画像はまだ追加していないので標準でXcodeに入っているものが出ます。自分で画像を追加していくこともできます。
ThirdViewController.storyboard
にも同様の手順で追加します。
FirstViewController.storyboard
についても本来同様の手順でよかったのですが、今回UINavigationController
が追加されています。
そのためFirstViewController
オブジェクトに追加しても表示されません。
この場合はNavigationController
オブジェクトに追加してあげてください。
[Is Initial ViewController]のチェックを入れたオブジェクトに追加する必要があると覚えましょう。
シミュレータで実行するとタブアイテムが表示されます。
UITabBarControllerについて
UITabBarController
もUINavigationController
と同様に画面を管理するクラスです。
ですがあくまで並列な画面遷移のためrootというものは存在しません。
表示される画面は同様にviewControllers
で管理されています。
タブ遷移ではプッシュ遷移と違い画面を消すということが基本的にはないためviewControllers
はスタック管理されているというわけではありません。
ただ配列として画面を持っているというだけです。また
UIViewController
はtabBarController
を持っており、それぞれのViewControllerが同一のUITabBarController
を参照しています。プッシュ遷移とタブ遷移の組み合わせ
実際のアプリではプッシュ遷移とタブ遷移両方を組み合わせて画面遷移を実装していきます。
また、今回のFirstViewController
ではすでに両方の遷移をしています。
FirstViewController
にはNavigationController
オブジェクトが追加されているからですね。今回の画面の構成を正確に書くと以下のようになります。
若干複雑な関係となります。図を見ると
NavigationController
もtabBarController
というプロパティを持っています。
これはUINavigationController
がUIViewController
を継承したクラスのためです。
同様にUITabBarController
もUIViewController
を継承しているためnavigationController
というプロパティを持ちます。
そのためさらにTabBarController
にNavigationController
を持たせることもできます。この図をいますぐ理解できる必要はありませんが、実際にアプリを作る際には自分がどんな構成の画面遷移を実装しているのか図に起こせるように理解できるようにしましょう。
最後に
今回でiOSの画面遷移編は終了です。
モーダル、プッシュ、タブそれぞれの遷移に意味があり実装方法が多少異なります。
積極的に使って自分のものにしていきましょう。最後の画面遷移の組み合わせはiOSを学ぶ上で躓く部分かもしれません。
これは画面遷移を実装→図に起こす→画面遷移を実装...と繰り返して感覚的に身につけていってください。次回はライフサイクルという画面表示非表示になる際にコールされるイベントについて説明します。
本連載ではプログラミング未経験からiOSアプリ開発が行えるようになることを目的としています。
今までの投稿をまとめていますのでこちらもご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684
- 投稿日:2020-05-30T18:24:07+09:00
[Swift]Eurekaのリスト選択に独自のViewControllerを利用する
背景
先日、ScoreBoxという麻雀成績管理アプリを個人開発してリリースしました。
その中の設定画面ではEurekaを使用しています。
Eurekaは非常にお手軽に設定画面を作成できるのでおすすめです。悩んだ
麻雀というからには4人(または3人)のプレイヤー選択が必要です。
Eurekaでは決まったリストからのマルチ選択機能は標準で用意されていましたが、
今回はリスト表示後も選択できるプレイヤー数をその場で増減させたかったので工夫が必要でした。工夫した
だったら自分でプレイヤー選択のViewControllerを自作し、
Eurekaから呼び出そうということでやったのがこちら。
呼び出し先のViewControllerの内容は割愛していますが、Controller内でプレイヤーリストの編集が可能になっています。<<< ButtonRow() { $0.title = "\(game.playerList.count)人 選択中" $0.hidden = false $0.presentationMode = .show(controllerProvider: ControllerProvider<UIViewController>.callback { return playerListVC }, onDismiss: { vc in vc.navigationController?.popViewController(animated: true) }) } .cellSetup{ (cell, row) in cell.imageView?.image = UIImage("アイコン画像") } .cellUpdate({ (cell, row) in cell.textLabel?.text = "\(game.playerList.count)人 選択中" })実際の画面
ポイント
$0.presentationMode = .show(controllerProvider: ControllerProvider<UIViewController>.callback { return playerListVC }, onDismiss: { vc in vc.navigationController?.popViewController(animated: true) })EurekaのButtonRowがタップされた際に表示する自作のViewControllerを指定し、
.cellUpdate({ (cell, row) in cell.textLabel?.text = "\(game.playerList.count)人 選択中" })自作のViewControllerから戻ってきた場合にButtonRowのキャプションを更新しています。
今後も開発中に工夫した点を残していきたいと思います。(需要が分からないので主に自分への備忘)
- 投稿日:2020-05-30T09:11:04+09:00
こんなソースコードはイヤだ-逆の意味のプロパティを用意してみる
- 投稿日:2020-05-30T08:12:56+09:00
CloudKit クラウドデータベースに公開情報を保存
この記事の内容
サーバーに変数を保存してその内容をアプリがフェッチできるようにしたいと思ったことはありませんか?
そんな時にぴったりなのが、CloudKit public databaseです。これはAppleから提供されている無料のサービスで、
iOS
、MacOS
、JaveScript Web (CloudKit JS)
環境でのみ利用できます。
CloudKit
についてこちらに私の以前の記事から
CloudKit
に関する説明を転載しました。長所
- データをプライベート・データベース、共有データベース、またはパブリック・データベース(すべてのユーザーが他のユーザーのレコードをフェッチできる)に保存できます。
- CloudKitはAppleの無料クラウドサービスです。
- CloudKitパブリック・データベースへの保存は、割り当て制限に対してカウントされます。
- ユーザーがアプリを削除してから再度インストールすると、CloudKitに保存されているデータをフェッチできます。
短所
- ユーザーはオンラインである必要があります。また、CloudKitデータベースの使用にはサイズ制限があります。
https://developer.apple.com/icloud/cloudkit/
この記事について
この記事では
CloudKit
のパブリックなデータベースについて話します。このデータベース上のデータはアプリのユーザー全員がアクセスすることができます。ユーザーのプライベートな情報を保存したい場合は、
CloudKit
のプライベートなデータベースが必要です。詳しくは私の以前の記事をご参照ください
CloudKit
の機能を有効化します。次にコンテナを作成します。私がここで以前書いた記事のステップ1のセクションをお読みください。
その記事のステップ2のところまで来たらこの記事に戻って下さい。
ステップ2. ユーザー情報を保存するため、CloudKitのデータ構造を設定する。
- 次に"CloudKit Dashboard"ボタンをクリックするか、http://icloud.developer.apple.com へ行く。
パネル左側にある自分のアプリケーション名をクリックする。
"Schema" をクリックします
"New Type" をクリックします
スキームを追加します
この例では販売する品物とその価格のリストを保存していきます。これが使おうとしているデータスキームです。
名称 種別 数値例 itemName String "ペット小屋" itemPrice Int(64) 3000 Int(64) が保存できる最大値は 2e63 − 1 です。従って Int(64) を使って価格を保存するのなら問題はないものと思われます。
CloudKit
のダッシュボード上にレコードタイプを作成する
新規レコードタイプの名称を
Price
としましょう作成した新規レコードタイプを選択してください。
Add Field
をクリックして新規のデータフィールドを追加します:
- 上記のフィールドを追加してください。手順が完了すると以下のようになります:
いま作成したレコードタイプをインデクシング(索引化)可能としてください
公開データベースでデータを閲覧するためには、索引化を有効にする必要があります:
Click on the
Edit Indexes
button
Add Indexes
ボタンをクリックしてくださいユーザーの便宜を図るため、ダッシュボードインターフェイスが索引として自動的に
recordName
を選択します。青いSave Changes
ボタンをクリックするだけで済みます。公開データベースで既存のレコードを取得する
上部の
Schema
ボタンをクリックし、Data
をクリックしてください通知: 開発環境を使用しているため、ここで加えた変更は (アプリストアに公開される) プロダクションアプリケーションでアクセス可能ではありません。この記事の後半で、開発からプロダクションにスキームを展開する方法を説明します。また、プロダクション環境で記録を作成する必要があります。
パネルの左側で、データベースを
Public Database
に切り替えてくださいそして、タイプを先ほど作成したデータベースのタイプに切り替えます。
Price
ですこのタイプのレコードが現在存在しないことが表示されます。
Create New Record
ボタンををクリックして作成してください。これで、
ペット小屋, 3000
のようなサンプルデータを入力することができます。そして、下部の青い
Save
ボタンをクリックし、このレコードを保存します。さて、再び
Query Records
をクリックすると、先ほど作成したレコードが表示されます。ここで、レコード名(そのデータベースレコードのID)が
31CEB769-8DC1-48FB-69C3-10CFC69C43F6
であることを覚えておいてください。これは後で必要になります。(追加ステップ) 新たなレコードタイプを作成してアイテムのコレクションを保持する
これは、私たちが今使っている実例の場合に有効な追加ステップです。この場合、あるアイテム列が含まれるようなデータタイプを新たに一つ作成します。これで、アイテムのコレクションを指し示すそのデータベースのレコードのIDをエンコードできます。さらに、弊社のアプリが一般公開された後でも、そのコレクションを後から更新することが可能になります。
コレクションデータタイプの作成:
上記と同じ手順で、このデータタイプにクエリインデックスを追加することも忘れないでください。
コレクション・レコードの作成:
上記のレコードIDを使用して、アイテムのコレクションを含むレコードを作成します。
このレコードの名前は
88F7E1D5-F8C5-5CEC-1253-27937513CB46
であることを覚えておいてください。アプリでレコードにアクセスする
さて、これらのレコードをiOSアプリで取得します:
レコードをフェッチするには
func fetchItem(itemID: String) { let db = CKContainer(identifier: "iCloud.com.[アプリiCloudコンテナーの名前]").publicCloudDatabase let recordID = CKRecord.ID(recordName: itemID) db.fetch(withRecordID: recordID) { (obtainedRecord, error) in if let itemName = obtainedRecord?.value(forKey: "itemName") as? String, let itemPrice = obtainedRecord?.value(forKey: "itemPrice") as? Int64 { print("Item Name \(itemName) with price \(itemPrice)") } } }
fetchItem(itemID: "31CEB769-8DC1-48FB-69C3-10CFC69C43F6")
を呼び出すと以下の結果が得られます:Item Name ペット小屋 with price 3000アイテムのコレクションをフェッチするには
func fetchCollections() { let appCollectionID = "88F7E1D5-F8C5-5CEC-1253-27937513CB46" let db = CKContainer(identifier: "iCloud.com.shunzhema.theclass").publicCloudDatabase let recordID = CKRecord.ID(recordName: appCollectionID) db.fetch(withRecordID: recordID) { (fetchedCollection, error) in if let itemIDs = fetchedCollection?.value(forKey: "itemRecordIDs") as? [String] { for itemID in itemIDs { fetchItem(itemID: itemID) } } } }さて、このファンクションを再び実行すると、以下のアウトプットを得ます:
Item Name ぬいぐるみ with price 3500 Item Name ペット小屋 with price 3000プロダクションにデプロイする
最後にはiCloudの開発用データベースを本番環境にデプロイする必要があります。
データベース構造のみがプロダクションにデプロイされることにご注意ください。開発データベースのデータはプロダクションデータベースにコピーされません。また、レコードのIDも異なるものになります。
デバッグのオプション
CloudKit
環境をデバッグする方法はいくつかありますライブログ
CloudKit
ダッシュボードで、一番上のドロップダウンメニューをクリックしてLog
を選択します環境の切り替え
シミュレーターでもプロダクション環境への切り替えが可能です。
Core Data
をCloudKit
と使う際にはよい方法ではないかもしれません。Core Data
がローカルのCore Data
のデータベース構造とCloudkit
を同期することを妨げる可能性があるからです。プロダクション環境を使うには、
.entitlement
ファイルを探し、以下のラインを加えます。<key>com.apple.developer.icloud-container-environment</key> <string>Production</string>
- 投稿日:2020-05-30T08:12:56+09:00
CloudKit クラウドデータベースに公開情報を保存 (CloudKit Public Database)
この記事の内容
サーバーに変数を保存してその内容をアプリがフェッチできるようにしたいと思ったことはありませんか?
- 商品の価格
- 掲載された記事のリスト
- その他...
そんな時にぴったりなのが、CloudKit public databaseです。これはAppleから提供されている無料のサービスで、
iOS
、MacOS
、JaveScript Web (CloudKit JS)
環境でのみ利用できます。
CloudKit
についてこちらに私の以前の記事から
CloudKit
に関する説明を転載しました。長所
- データをプライベート・データベース、共有データベース、またはパブリック・データベース(すべてのユーザーが他のユーザーのレコードをフェッチできる)に保存できます。
- CloudKitはAppleの無料クラウドサービスです。
- CloudKitパブリック・データベースへの保存は、割り当て制限に対してカウントされます。
- ユーザーがアプリを削除してから再度インストールすると、CloudKitに保存されているデータをフェッチできます。
短所
- ユーザーはオンラインである必要があります。また、CloudKitデータベースの使用にはサイズ制限があります。
https://developer.apple.com/icloud/cloudkit/
この記事について
この記事では
CloudKit
のパブリックなデータベースについて話します。このデータベース上のデータはアプリのユーザー全員がアクセスすることができます。ユーザーのプライベートな情報を保存したい場合は、
CloudKit
のプライベートなデータベースが必要です。詳しくは私の以前の記事をご参照ください
CloudKit
の機能を有効化します。次にコンテナを作成します。私がここで以前書いた記事のステップ1のセクションをお読みください。
その記事のステップ2のところまで来たらこの記事に戻って下さい。
ステップ2. ユーザー情報を保存するため、CloudKitのデータ構造を設定する。
- 次に"CloudKit Dashboard"ボタンをクリックするか、http://icloud.developer.apple.com へ行く。
パネル左側にある自分のアプリケーション名をクリックする。
"Schema" をクリックします
"New Type" をクリックします
スキームを追加します
この例では販売する品物とその価格のリストを保存していきます。これが使おうとしているデータスキームです。
名称 種別 数値例 itemName String "ペット小屋" itemPrice Int(64) 3000 Int(64) が保存できる最大値は 2e63 − 1 です。従って Int(64) を使って価格を保存するのなら問題はないものと思われます。
CloudKit
のダッシュボード上にレコードタイプを作成する
新規レコードタイプの名称を
Price
としましょう作成した新規レコードタイプを選択してください。
Add Field
をクリックして新規のデータフィールドを追加します:
- 上記のフィールドを追加してください。手順が完了すると以下のようになります:
いま作成したレコードタイプをインデクシング(索引化)可能としてください
公開データベースでデータを閲覧するためには、索引化を有効にする必要があります:
Add Indexes
ボタンをクリックしてくださいユーザーの便宜を図るため、ダッシュボードインターフェイスが索引として自動的に
recordName
を選択します。青いSave Changes
ボタンをクリックするだけで済みます。公開データベースで既存のレコードを取得する
上部の
Schema
ボタンをクリックし、Data
をクリックしてください通知: 開発環境を使用しているため、ここで加えた変更は (アプリストアに公開される) プロダクションアプリケーションでアクセス可能ではありません。この記事の後半で、開発からプロダクションにスキームを展開する方法を説明します。また、プロダクション環境で記録を作成する必要があります。
パネルの左側で、データベースを
Public Database
に切り替えてくださいそして、タイプを先ほど作成したデータベースのタイプに切り替えます。
Price
ですこのタイプのレコードが現在存在しないことが表示されます。
Create New Record
ボタンををクリックして作成してください。これで、
ペット小屋, 3000
のようなサンプルデータを入力することができます。そして、下部の青い
Save
ボタンをクリックし、このレコードを保存します。さて、再び
Query Records
をクリックすると、先ほど作成したレコードが表示されます。ここで、レコード名(そのデータベースレコードのID)が
31CEB769-8DC1-48FB-69C3-10CFC69C43F6
であることを覚えておいてください。これは後で必要になります。(追加ステップ) 新たなレコードタイプを作成してアイテムのコレクションを保持する
これは、私たちが今使っている実例の場合に有効な追加ステップです。この場合、あるアイテム列が含まれるようなデータタイプを新たに一つ作成します。これで、アイテムのコレクションを指し示すそのデータベースのレコードのIDをエンコードできます。さらに、弊社のアプリが一般公開された後でも、そのコレクションを後から更新することが可能になります。
コレクションデータタイプの作成:
上記と同じ手順で、このデータタイプにクエリインデックスを追加することも忘れないでください。
コレクション・レコードの作成:
上記のレコードIDを使用して、アイテムのコレクションを含むレコードを作成します。
このレコードの名前は
88F7E1D5-F8C5-5CEC-1253-27937513CB46
であることを覚えておいてください。アプリでレコードにアクセスする
さて、これらのレコードをiOSアプリで取得します:
レコードをフェッチするには
func fetchItem(itemID: String) { let db = CKContainer(identifier: "iCloud.com.[アプリiCloudコンテナーの名前]").publicCloudDatabase let recordID = CKRecord.ID(recordName: itemID) db.fetch(withRecordID: recordID) { (obtainedRecord, error) in if let itemName = obtainedRecord?.value(forKey: "itemName") as? String, let itemPrice = obtainedRecord?.value(forKey: "itemPrice") as? Int64 { print("Item Name \(itemName) with price \(itemPrice)") } } }
fetchItem(itemID: "31CEB769-8DC1-48FB-69C3-10CFC69C43F6")
を呼び出すと以下の結果が得られます:Item Name ペット小屋 with price 3000アイテムのコレクションをフェッチするには
func fetchCollections() { let appCollectionID = "88F7E1D5-F8C5-5CEC-1253-27937513CB46" let db = CKContainer(identifier: "iCloud.com.[アプリiCloudコンテナーの名前]").publicCloudDatabase let recordID = CKRecord.ID(recordName: appCollectionID) db.fetch(withRecordID: recordID) { (fetchedCollection, error) in if let itemIDs = fetchedCollection?.value(forKey: "itemRecordIDs") as? [String] { for itemID in itemIDs { fetchItem(itemID: itemID) } } } }さて、このファンクションを再び実行すると、以下のアウトプットを得ます:
Item Name ぬいぐるみ with price 3500 Item Name ペット小屋 with price 3000プロダクションにデプロイする
最後にはiCloudの開発用データベースを本番環境にデプロイする必要があります。
通知: 開発環境を使用しているため、ここで加えた変更は (アプリストアに公開される) プロダクションアプリケーションでアクセス可能ではありません。プロダクション環境で記録 (Record) を作成する必要があります。
デバッグのオプション
CloudKit
環境をデバッグする方法はいくつかありますライブログ
CloudKit
ダッシュボードで、一番上のドロップダウンメニューをクリックしてLog
を選択します環境の切り替え
シミュレーターでもプロダクション環境への切り替えが可能です。
Core Data
をCloudKit
と使う際にはよい方法ではないかもしれません。Core Data
がローカルのCore Data
のデータベース構造とCloudkit
を同期することを妨げる可能性があるからです。プロダクション環境を使うには、
.entitlement
ファイルを探し、以下のラインを加えます。<key>com.apple.developer.icloud-container-environment</key> <string>Production</string>
- 投稿日:2020-05-30T06:53:32+09:00
1つの UIViewController 内の複数の UITableView
この記事で説明すること
今日は、主に
UITableView
の使い方を見ていきます。
UITabeViewDelegate
とUITableViewDataSource
クラスを作成して、現在のコードを単純化します- このカスタムクラスを使用して、2つ以上のテーブルビューを1つの
UIViewController
に埋め込みます。通常の構造
通常、既存の
UIViewController
にUITableView
を追加する場合は、次のように行います。@IBOutlet weak var tableView: UITableView!extension ViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { //やること:テーブルビューの行数を返す } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { //やること:テーブルビューセルを生成する } }
UITableViewDelegate
とUITableViewDataSource
にある関数を別のクラスに抽出することができますclass tableDataSource: NSObject, UITableViewDelegate, UITableViewDataSource { var items = ... func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let item = items[indexPath.row] ... return cell } }これにより、画面に表示する
UITableView
ごとに1つのtableDataSource
を作成できます。したがって、このコードを使用して複数のUITableView
を作成できますここではデフォルトのシステムが作り出す
UITableViewCell
を使用できます:タイトルのみ
let cell = UITableViewCell(style: .default, reuseIdentifier: nil) cell.textLabel?.text = item.title return cellタイトルとサブタイトル
let cell = UITableViewCell(style: .default, reuseIdentifier: nil) cell.textLabel?.text = item.title cell.detailTextLabel?.text = item.subtitle return cell画像込みで
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) cell.textLabel?.text = item.title cell.detailTextLabel?.text = item.subtitle cell.imageView?.image = UIImage(systemName: "person.circle.fill") return cell例
そして、
tableDataSource
を使用して、2つのUITableView
を1つのUIViewController
に追加します。オブジェクトの構造
struct Item { var title: String var subtitle: String }テーブルビューのデータソース
class tableDataSource: NSObject, UITableViewDelegate, UITableViewDataSource { var items = [Item]() func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let item = items[indexPath.row] let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) cell.textLabel?.text = item.title cell.detailTextLabel?.text = item.subtitle return cell } }変数
@IBOutlet weak var tableView1: UITableView! var tableView1Data = tableDataSource() @IBOutlet weak var tableView2: UITableView! var tableView2Data = tableDataSource()データ
サンプルデータ
//Sample items let cat1 = Item(title: "ネコノヒー", subtitle: "ネコ") let cat2 = Item(title: "ムギ", subtitle: "ネコ") let cat3 = Item(title: "レオ", subtitle: "ネコ") let dog1 = Item(title: "ソラ", subtitle: "犬") let dog2 = Item(title: "マル", subtitle: "犬") //Set the data tableView1Data.items = [cat1, cat2, cat3] tableView2Data.items = [dog1, dog2]テーブルビューの設定
//tableView1 tableView1.delegate = tableView1Data tableView1.dataSource = tableView1Data //tableView2 tableView2.delegate = tableView2Data tableView2.dataSource = tableView2Dataプログラム実行時のスクリーンショットはこちら:
次のステップ
これで
tableDataSource
を再利用できるので、複数のUITableView
を表示するのに書くコードを減らせます。