20200530のSwiftに関する記事は9件です。

【プログラミング初心者】Swift基礎~値型・参照型~

はじめに

クラスと構造体の違いについて説明します。
Swift基礎~構造体・クラス~という記事で使った構造体やクラスを使って説明していくのでまずそちらを参照して頂ければと思います。

今回の内容はC言語でいうところのポインタ、動的メモリ確保です。
今対象にしているSwiftなどの言語では意識することが少ない内容となります。
言い換えるとそこまで理解していなくてもSwiftならある程度実装できるので難しければ保留にしておいてもいいかもしれません。

メモリについて

前提知識として少しメモリの話をします。

まずメモリを扱うには確保開放という操作が必要になります。
そのままの意味ですが確保すると「今からこの領域を使います」というようにそのデータが対象のメモリを専有します。開放はその専有をやめることです。使用されているメモリは他から急に書き換えられると困るので開放されるまでは他からは使えません。

よく一口にメモリと言っていますが、OSはこのメモリをスタック領域ヒープ領域に分けて管理しています。
スタック領域とヒープ領域は、どちらも一時的に確保されるメモリ領域ですがそれぞれ管理方法が異なります。

  • スタック領域
    • メモリがA→B→Cの順で確保された場合、C→B→Aの順で解放される。メモリをいわゆるスタックで管理している。
      処理ブロック({}で囲まれたブロック)を抜けると解放されるという特長がある。
      ヒープよりも割り当てられている領域が小さい。
  • ヒープ領域
    • 確保されたメモリは明示的に解放しないとずっと留まり続ける。そのため気をつけていなければメモリリークを引き起こしクラッシュの原因になる。
      自由なサイズのメモリを確保できるというのが特長。

変数の参照先は全てスタックに格納された値です。
ここまでは前提知識として覚えておいてください。

クラスと構造体の違い

クラスと構造体は若干プログラムの定義の仕方が異なる部分もありますが基本的には同じように扱えます。
ですが根本的に異なる部分があります。
それは実体化したときのメモリの取られ方です。

構造体、クラスはそれぞれ値型参照型と呼ばれます。

値型

まず値型と呼ばれる構造体について。
Swift基礎~構造体・クラス~で作ったCompany構造体を例にして説明していきます。
以前変数のメモリの取られ方について説明しました。
このときはIntを題材にしましたが、定義を見ればわかりますが実はIntも構造体です。
なのでメモリの長さは異なりますが、以前説明したメモリの取られ方と基本的には同じです。
詳細な取られ方はわかりませんが構造体は以下のようなイメージでメモリが確保されます。
構造体.png

値型、つまり構造体の場合値の全てがスタック領域に確保されます。
例えば以下のような宣言をした場合

var company: Company = Company(name: "株式会社山田商事",
                               president: "山田太郎",
                               earnings: 1000000,
                               departments: [])

以下のようなイメージになります。
値型1.png
これは特に疑問なくイメージできるかと思います。

今度は以下の場合を見てみましょう。

var company: Company = Company(name: "株式会社山田商事",
                               president: "山田太郎",
                               earnings: 1000000,
                               departments: [])
var tmpCompany = company

この場合はどうでしょう?
これもスタックに確保されるのですが、新しく確保された領域にコピーされた値が格納されます。
値型2.png
値型の場合変数には値そのものが入っています。
そのため新しく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にはオブジェクトのアドレスが格納されています。
図で表すと以下の様になります。
参照型1.png
このように値そのものではなく、オブジェクトへの参照を変数に格納することから参照型と呼ばれています。
(C言語ではポインタ型と言われ、この場合Humanポインタ型と呼びます)

では以下のようにhumanを新しい変数に格納した場合はどのようになるでしょう?

var human = Human(name: "山田太郎", age: 25)
var tmpHuman = human

何度か言っているように変数はスタックに確保されます。
そして値型どうように変数宣言をすると新しくスタック領域に値のコピーが作成されます。
つまり以下のようになります。
参照型2.png
このように変数にはアドレスという値がコピーされ、実際指し示すアドレスは同じオブジェクトなのでhumantmpHumanは同じものと見なすことができます。

確認してみましょう。
以下のコードで出力を見てみます。

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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]

:thumbsup:

そしてツールの完成形はこちら

Cleaner.swift
import 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.swift
let cleaner = Cleaner()
cleaner.run()

swift run で実行する事ができます。

オプションを指定できるようにする

Device Support に関しては使用中のものもあると思うので試しにオプションでスキップできるように
してみたいと思います。

PathKitと同じ作者の Commander を使用します。

Package.swiftCommander を追加

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.swift
import 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NeumorphismなUIButton

gifは、影の大きさを調節する関数を実装する前のものです。

はじめに

NeumorphismなUIButtonを作ったときのことを記録に残しておきます。
index.gif

このButtonのしくみ

見てもらうとすぐわかるんですが、
image.png
クリックされていないときは、このボタンは

  • ボタンの外側の上と左に光があたっている。
  • ボタンの外側の下と右に影がついている。

というような特徴をもっています。
それだけでなく、

image.png

  • 押されているときはボタンの内側の上と左に影がついている。
  • 押されているときはボタンの内側の下と右に光があたっている。

というような特徴をもっています。

この計4つの特徴さえわかってしまえば、あとはそのとおりにUIButtonをいじっていくだけです。

実際のコード

実際のコードを公開します。

  • Colors.swift
  • PlainSquareButton.swift

という2つのファイルを使って実装しました。

Colors.swift
import 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.swift
import 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)くらいの位置まで届くように表示しています。
すると、とりあえずこの画像の状態が完成します。

image.png

凹ませた状態を作るのがちょっと厄介で、
まずCAGradientLayerを使って、始点の方に光、終点の方に影があたるようにグラデーションを設定します。
これを縦と横につけるのですが、locationsの数値を一緒にしていると、影の長さが縦と横で合わなくなってしまうので、ざっくりと合うようにgetProperHeightLocations()のようなメソッドを用意しておきました。
そして作成されたレイヤを貼り付けることで、凹んだ状態を再現できます。
表示されていると困る方のレイヤがremoveしてしまいましょう。

image.png

これで、gifのようなボタンが再現できると思います。

おわり

けっこう楽しいので、この調子で色々な部品を作ってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】iOSアプリ開発入門~ 画面遷移編3~

はじめに

前回はiOSアプリにおける画面遷移の種類についてお話しました。
画面遷移編2:https://qiita.com/euJcIKfcqwnzDui/items/6d37aaf00c0bc7ce26ca

今回はその具体的な実装方法について説明していきます。
以前モーダル遷移の実装をしたプロジェクトをベースとして説明していきます。

前準備

その前に、今までプロジェクト作成されたときに生成されたViewController.swiftMain.storyboardをそのまま使ってきましたが、画面が多くなるとわかりにくくなるので変更しておきましょう。
それぞれFirstViewController.swiftFirstViewController.storyboardにリネームします。

まずはファイル名をリネームします。
ファイルを選択しもう一度ファイル名をクリックするとリネームできます。
スクリーンショット 2020-05-30 17.23.30.png

次はクラス名を変更します。

FirstViewController.swift
import UIKit

class FirstViewController: UIViewController { // ViewControllerから変更
    /// テキストフィールド
    @IBOutlet weak var textField: UITextField!

    /// 画面遷移ボタンタップ処理
    /// - Parameter sender: ボタン
    @IBAction func didTapTransitionButton(_ sender: Any) {
...
...

クラス名を変更すると今までそのクラスを使用していた場所も変更してあげないといけません。
今の状態だとFirstViewController.storyboardViewControllerオブジェクトに設定されています。
FirstViewController.storyboardを開きViewControllerオブジェクトを選択します。
その状態で[Identity Inspector]を選択し、[class]の項目をFirstViewControllerと入力します。
スクリーンショット 2020-05-30 17.30.10.png
ここで一度シミュレータで実行してみましょう。
すると、起動直後アプリがクラッシュし以下のエラーが表示されます。

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を選択してください。
スクリーンショット 2020-05-30 17.39.52.png
さらにXcode11からInfo.plistというファイルにも「Main」が設定されています。
その中の[Storyboard Name]の項目を「FirstViewController」と書き換えてください。
スクリーンショット 2020-05-30 17.51.01.png

これでファイル名の変更は完了です。
シミュレータで実行し問題ないか確認してください。

モーダル

これは以前説明したので省きます。
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]を選択していってください。
スクリーンショット 2020-05-29 23.31.34.png
FirstViewControllerにくっつくような形でNavigation Controllerが追加されました。
スクリーンショット 2020-05-29 23.33.39.png
Navigation Controllerは画面のような見た目をしていますが、これはあくまでStoryboard上の表示での話であって実際はユーザから見られる画面というわけではありません。

UINavigationControllerControllerとあるようにコントロールするためのクラスです。
Navigationは画面遷移と言い換えてもらえればいいかと思います。
つまりUINavigationControllerは画面遷移を制御するためのクラスであって画面そのものではありません。
今のNavigation Controllerを追加した操作はUINavigationControllerという概念のようなオブジェクトをFirstViewControllerに追加するというイメージです。

もう少し補足するとUIViewControllerもControllerとあるように、これ自体が画面というわけではありません。
これも画面をコントロールするオブジェクトで、実際に表示されている画面はFirstViewControllerの中にあるViewというUIです。
コントローラは画面の表示制御やユーザからのイベントを扱うという役割です。

少し細かい話をしてしまいました。
実装を進めます。

FirstViewControllerにあるsegueを選択してください。
その状態で[Attributes Inspector]を選択すると[Kind]という項目があるので選択します。
スクリーンショット 2020-05-30 0.06.08.png
ポップアップメニューが表示されるので[Show(e.g. Push)]を選択します。
これでsegueがプッシュ遷移用に変更されました。
スクリーンショット 2020-05-30 0.10.48.png
Storyboardの設定はこれだけです。

あとはソースコードから処理実行するだけです。
ですがこれはモーダル遷移の場合と同じでperformSegueを呼び出してあげるだけです。
segueのidentifierを変えていないので以前モーダルを実装したままのコードですでにプッシュ遷移ができます。
該当のコードは以下です。

FirstViewController.swift
self.performSegue(withIdentifier: "SecondViewController", sender: nil)

実装は以上です。
シミュレータで実行し動作を確認してみてください。

またパラメータもモーダルと同様の方法で渡すことができます。

FirstViewController.swift
let text = textField.text
self.performSegue(withIdentifier: "SecondViewController", sender: text)

UINavigationControllerについて

UINavigationControllerでは一連のプッシュ遷移における画面を管理します。
UINavigationControllerrootViewControllerというプロパティを持ち、プッシュ遷移の一番最初の画面を保持します。
Storyboardから追加した今回の場合だと、UINavigationControllerを追加したFirstViewControllerrootViewControllerになります。

またUINavigationControllerviewControllers: [UIViewController]というプロパティを持っており、遷移した画面を配列で管理しています。
今回の例では画面遷移が完了した状態ではviewControllers[0]FirstViewControllerviewControllers[1]SecondViewControllerが格納されています。

プッシュ遷移先のSecondViewControllerに[戻る]ボタンが表示されていたかと思います。
[戻る]ボタンをタップすると一画面前に戻ります。
画面を戻るとSecondViewControllerUINavigationControllerの管理対象からはずれ解放されます。
つまりviewControllers[1]は存在しなくなります。
図で表すと以下のようになります。
プッシュ画面遷移前.png
プッシュ画面遷移後.png
プッシュ戻るボタンタップ後.png

SecondViewControllerからさらに遷移する

当然SecondViewControllerからThirdViewControllerという画面にさらにプッシュ遷移した場合管理下に追加されviewControllers[2]に格納されます。

実装してみます。
これは特に難しい話ではなく同様にSecondViewControllerからThirdViewControllerへのsegueを作ってあげてperformSegueを呼び出してあげるだけです。

SecondViewControllerを追加した手順と同様にThirdViewController.swiftThirdViewController.storyboardを追加以下のように編集してください。

ThirdViewController.swift
import UIKit

class ThirdViewController: UIViewController {

}

スクリーンショット 2020-05-30 14.16.10.png

さらにSecondViewController.storyboardからThirdViewControllerへのsegueをプッシュで作成してください。
またSecondViewController.storyboardにボタンを追加し、ボタンタップでThirdViewControllerに遷移するようにします。
シミュレータで実行すると3画面遷移できるようになります。
スクリーンショット 2020-05-30 14.21.33.png

SecondViewController
/// 画面遷移ボタンタップ処理
/// - Parameter sender: ボタン
@IBAction func didTapTransitionButton(_ sender: Any) {
    // 画面遷移
    self.performSegue(withIdentifier: "ThirdViewController", sender: nil)
}

ここで注意してほしいことはSecondViewController.storyboardUINavigationControllerは追加しないということです。
UINavigationControllerはrootとなる画面にのみ追加します。
今rootはFirstViewControllerとしているためSecondViewControllerはrootではありません。
SecondViewControllerからプッシュ遷移する場合、rootに設定したUINavigationControllerが参照されます。

画面遷移後は以下のような状態になります。
プッシュ遷移3画面.png

予想がつくかと思いますがThirdViewControllerで[戻る]ボタンを押すと以下のような構成になります。
Third戻る.png
このようにUINavigationControllerでは最後に追加されたものから消えていきます。
この管理方法をスタック(stack)と呼びます。
余談ですがスタックは画面管理の方法ではなく配列の管理方法の1つです。
スタックは基本情報試験にも出てくるIT技術の基礎知識なので抑えておきましょう。
別の配列管理方法ではキュー(queue)があります。一度調べておいてください。

UINavigationControllerの参照のされ方

ここまでの画面遷移処理で少し違和感を覚える方もいるかもしれません。

rootであるFirstViewControllerはStoryboardでUINavigationControllerのオブジェクトを追加したので画面遷移にUINavigationControllerを使えるのはなんとなくわかるかと思います。

ですがSecondViewControllerにはUINavigationControllerは設定されていないはずなのにどうして使えるのでしょうか?

実はUIViewControllerにはnavigationControllerというように現在自分が管理されているUINavigationControllerをプロパティとして持っています。
画面遷移の具体的な処理はUIKitが勝手にやってくれているので詳細は不明なのですが、おそらくプッシュ遷移のタイミングで自分のnavigationControllerを次の画面に一緒にセットしています。

従って1つのUINavigationControllerのオブジェクトが次へ次へと共有されているのです。
図にすると以下のようなイメージになります。
オブジェクト関係図.png
このような形でroot画面からプッシュ遷移した画面の全てを管理しています。

タブ

segueを使った実装

タブ遷移の実装方法について説明します。
タブに関してもsegueを使った実装をします。

タブの遷移にはUITabBarControllerというクラスを使用します。
UITabBarControllerを扱うために、まずTabViewController.storyboardというファイルを追加しましょう。
スクリーンショット 2020-05-30 18.15.50.png
TabViewController.storyboardを開き、[+]ボタンからUITabBarControllerを探しStoryboardに追加します。
スクリーンショット 2020-05-30 18.16.47.png
Tab Bar Controllerというオブジェクトと一緒にitem 1 Sceneitem 2 Sceneというオブジェクトが追加されました。
これはタブTab Bar Controllerがデフォルトで表示する画面テンプレートです。
今回は不要なので削除します。
それぞれ選択し削除しておいてください。
スクリーンショット 2020-05-30 18.18.23.png

Tab Bar Controllerも画面のような形ですが、これも「Controller」とあるように画面そのものではなくタブ遷移において画面を管理するオブジェクトです。
階層1つしたにあるTab Barというオブジェクトは画面一番下に表示されるタブ自体です。これは表示されるUIでUITabBarというクラスです。

このTabBarController.storyboardで最初に参照するオブジェクトはTabBarControllerなので[Is Initial View Controller]のチェックを付けます。
スクリーンショット 2020-05-30 18.24.11.png
TabBarControllerが画面を追加します。
FirstViewController.storyboardSecondViewController.storyboardThirdViewController.storyboardへの参照を追加します。
他の画面遷移と同様Storyboard Referenceを画面数だけ配置します。
配置したStoryboard ReferenceにそれぞれStoryboardを設定します。
スクリーンショット 2020-05-30 18.41.38.png
TabBarControllerからそれぞれのStoryboard Referenceまでsegueを作成します。
segueの種類は[Relationship Segue]の[view controllers]を選択してください。
スクリーンショット 2020-05-30 18.43.40.png
以下のようになればタブ遷移の準備は完了です。
スクリーンショット 2020-05-30 18.47.36.png
次はアプリ起動直後に表示するStoryboardを変更します。
先述したMain.storyboardをリネームする際、説明したようにプロジェクトの[Main Interface]とInfo.plistの[Storyboard Name]を「TabBarController」に変更してください。
スクリーンショット 2020-05-30 18.52.42.png
スクリーンショット 2020-05-30 18.53.10.png
シミュレータで実行するとタブバーが表示されます。
タブバーには何も表示されていませんが、タブボタン自体は配置されています。
適当な場所をタップするとタブ遷移されます。
スクリーンショット 2020-05-30 19.02.38.png
ですがこれではユーザからすると何のUIかわからないので画像と名前を設定してあげましょう。

SecondViewController.storyboardを開いてください。
[+]ボタンからUITabBarItemを探し、SecondViewControllerに追加してください。
UITabBarItemはタブボタンのオブジェクトです。
ここにアイテム名や画像を設定してあげるとそれがタブに表示されます。
UITabBarItemを選択し、[Attributes Inspector]の[BarItem]に[Title]と[Image]を設定してください。
※画像はまだ追加していないので標準でXcodeに入っているものが出ます。自分で画像を追加していくこともできます。
スクリーンショット 2020-05-30 19.12.11.png
ThirdViewController.storyboardにも同様の手順で追加します。

FirstViewController.storyboardについても本来同様の手順でよかったのですが、今回UINavigationControllerが追加されています。
そのためFirstViewControllerオブジェクトに追加しても表示されません。
この場合はNavigationControllerオブジェクトに追加してあげてください。
[Is Initial ViewController]のチェックを入れたオブジェクトに追加する必要があると覚えましょう。
スクリーンショット 2020-05-30 19.23.02.png
シミュレータで実行するとタブアイテムが表示されます。
スクリーンショット 2020-05-30 19.26.01.png

UITabBarControllerについて

UITabBarControllerUINavigationControllerと同様に画面を管理するクラスです。
ですがあくまで並列な画面遷移のためrootというものは存在しません。
表示される画面は同様にviewControllersで管理されています。
タブ遷移ではプッシュ遷移と違い画面を消すということが基本的にはないためviewControllersはスタック管理されているというわけではありません。
ただ配列として画面を持っているというだけです。

またUIViewControllertabBarControllerを持っており、それぞれのViewControllerが同一のUITabBarControllerを参照しています。

図にすると以下のようなイメージです。
タブ関係図.png

プッシュ遷移とタブ遷移の組み合わせ

実際のアプリではプッシュ遷移とタブ遷移両方を組み合わせて画面遷移を実装していきます。
また、今回のFirstViewControllerではすでに両方の遷移をしています。
FirstViewControllerにはNavigationControllerオブジェクトが追加されているからですね。

今回の画面の構成を正確に書くと以下のようになります。
全体関係図.png
若干複雑な関係となります。

図を見るとNavigationControllertabBarControllerというプロパティを持っています。
これはUINavigationControllerUIViewControllerを継承したクラスのためです。
同様にUITabBarControllerUIViewControllerを継承しているためnavigationControllerというプロパティを持ちます。
そのためさらにTabBarControllerNavigationControllerを持たせることもできます。

この図をいますぐ理解できる必要はありませんが、実際にアプリを作る際には自分がどんな構成の画面遷移を実装しているのか図に起こせるように理解できるようにしましょう。

最後に

今回でiOSの画面遷移編は終了です。
モーダル、プッシュ、タブそれぞれの遷移に意味があり実装方法が多少異なります。
積極的に使って自分のものにしていきましょう。

最後の画面遷移の組み合わせはiOSを学ぶ上で躓く部分かもしれません。
これは画面遷移を実装→図に起こす→画面遷移を実装...と繰り返して感覚的に身につけていってください。

次回はライフサイクルという画面表示非表示になる際にコールされるイベントについて説明します。

本連載ではプログラミング未経験からiOSアプリ開発が行えるようになることを目的としています。
今までの投稿をまとめていますのでこちらもご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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)人 選択中"
    })

実際の画面

20200530_173042.GIF

ポイント

$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のキャプションを更新しています。

今後も開発中に工夫した点を残していきたいと思います。(需要が分からないので主に自分への備忘)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

こんなソースコードはイヤだ-逆の意味のプロパティを用意してみる

プログラムのソースコードのより良い書き方をまとめていこうと思います。

逆の意味のプロパティを用意してみる

sample.swift
  self.button.isHidden = !(self.isMale && !self.isMember)

どのようにリファクタリングできるのか

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudKit クラウドデータベースに公開情報を保存

この記事の内容

サーバーに変数を保存してその内容をアプリがフェッチできるようにしたいと思ったことはありませんか?

そんな時にぴったりなのが、CloudKit public databaseです。これはAppleから提供されている無料のサービスで、iOSMacOSJaveScript 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" をクリックします

image.png

"New Type" をクリックします

image.png

スキームを追加します

この例では販売する品物とその価格のリストを保存していきます。これが使おうとしているデータスキームです。

名称 種別 数値例
itemName String "ペット小屋"
itemPrice Int(64) 3000

Int(64) が保存できる最大値は 2e63 − 1 です。従って Int(64) を使って価格を保存するのなら問題はないものと思われます。

CloudKit のダッシュボード上にレコードタイプを作成する

  • 新規レコードタイプの名称を Price としましょう

  • 作成した新規レコードタイプを選択してください。Add Field をクリックして新規のデータフィールドを追加します:

image

  • 上記のフィールドを追加してください。手順が完了すると以下のようになります:

image

いま作成したレコードタイプをインデクシング(索引化)可能としてください

公開データベースでデータを閲覧するためには、索引化を有効にする必要があります:

Click on the Edit Indexes button

image

Add Indexes ボタンをクリックしてください

image

ユーザーの便宜を図るため、ダッシュボードインターフェイスが索引として自動的に recordName を選択します。青い Save Changes ボタンをクリックするだけで済みます。

公開データベースで既存のレコードを取得する

上部の Schema ボタンをクリックし、Data をクリックしてください

image

通知: 開発環境を使用しているため、ここで加えた変更は (アプリストアに公開される) プロダクションアプリケーションでアクセス可能ではありません。この記事の後半で、開発からプロダクションにスキームを展開する方法を説明します。また、プロダクション環境で記録を作成する必要があります。

パネルの左側で、データベースを Public Database に切り替えてください

image

そして、タイプを先ほど作成したデータベースのタイプに切り替えます。Price です

image

このタイプのレコードが現在存在しないことが表示されます。Create New Record ボタンををクリックして作成してください。

image

これで、ペット小屋, 3000 のようなサンプルデータを入力することができます。

image

そして、下部の青い Save ボタンをクリックし、このレコードを保存します。

さて、再び Query Records をクリックすると、先ほど作成したレコードが表示されます。

image

ここで、レコード名(そのデータベースレコードのID)が 31CEB769-8DC1-48FB-69C3-10CFC69C43F6 であることを覚えておいてください。これは後で必要になります。

(追加ステップ) 新たなレコードタイプを作成してアイテムのコレクションを保持する

これは、私たちが今使っている実例の場合に有効な追加ステップです。この場合、あるアイテム列が含まれるようなデータタイプを新たに一つ作成します。これで、アイテムのコレクションを指し示すそのデータベースのレコードのIDをエンコードできます。さらに、弊社のアプリが一般公開された後でも、そのコレクションを後から更新することが可能になります。

コレクションデータタイプの作成:

image

上記と同じ手順で、このデータタイプにクエリインデックスを追加することも忘れないでください。

コレクション・レコードの作成:

上記のレコードIDを使用して、アイテムのコレクションを含むレコードを作成します。

image

このレコードの名前は 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の開発用データベースを本番環境にデプロイする必要があります。

image

データベース構造のみがプロダクションにデプロイされることにご注意ください。開発データベースのデータはプロダクションデータベースにコピーされません。また、レコードのIDも異なるものになります。

デバッグのオプション

CloudKit 環境をデバッグする方法はいくつかあります

ライブログ

CloudKit ダッシュボードで、一番上のドロップダウンメニューをクリックして Log を選択します

image

環境の切り替え

シミュレーターでもプロダクション環境への切り替えが可能です。

Core DataCloudKit と使う際にはよい方法ではないかもしれません。Core Data がローカルの Core Data のデータベース構造とCloudkit を同期することを妨げる可能性があるからです。

プロダクション環境を使うには、.entitlement ファイルを探し、以下のラインを加えます。

<key>com.apple.developer.icloud-container-environment</key>
<string>Production</string>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudKit クラウドデータベースに公開情報を保存 (CloudKit Public Database)

この記事の内容

サーバーに変数を保存してその内容をアプリがフェッチできるようにしたいと思ったことはありませんか?

  • 商品の価格
  • 掲載された記事のリスト
  • その他...

そんな時にぴったりなのが、CloudKit public databaseです。これはAppleから提供されている無料のサービスで、iOSMacOSJaveScript 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" をクリックします

image.png

"New Type" をクリックします

image.png

スキームを追加します

この例では販売する品物とその価格のリストを保存していきます。これが使おうとしているデータスキームです。

名称 種別 数値例
itemName String "ペット小屋"
itemPrice Int(64) 3000

Int(64) が保存できる最大値は 2e63 − 1 です。従って Int(64) を使って価格を保存するのなら問題はないものと思われます。

CloudKit のダッシュボード上にレコードタイプを作成する

  • 新規レコードタイプの名称を Price としましょう

  • 作成した新規レコードタイプを選択してください。Add Field をクリックして新規のデータフィールドを追加します:

image

  • 上記のフィールドを追加してください。手順が完了すると以下のようになります:

image

いま作成したレコードタイプをインデクシング(索引化)可能としてください

公開データベースでデータを閲覧するためには、索引化を有効にする必要があります:

image

Add Indexes ボタンをクリックしてください

image

ユーザーの便宜を図るため、ダッシュボードインターフェイスが索引として自動的に recordName を選択します。青い Save Changes ボタンをクリックするだけで済みます。

公開データベースで既存のレコードを取得する

上部の Schema ボタンをクリックし、Data をクリックしてください

image

通知: 開発環境を使用しているため、ここで加えた変更は (アプリストアに公開される) プロダクションアプリケーションでアクセス可能ではありません。この記事の後半で、開発からプロダクションにスキームを展開する方法を説明します。また、プロダクション環境で記録を作成する必要があります。

パネルの左側で、データベースを Public Database に切り替えてください

image

そして、タイプを先ほど作成したデータベースのタイプに切り替えます。Price です

image

このタイプのレコードが現在存在しないことが表示されます。Create New Record ボタンををクリックして作成してください。

image

これで、ペット小屋, 3000 のようなサンプルデータを入力することができます。

image

そして、下部の青い Save ボタンをクリックし、このレコードを保存します。

さて、再び Query Records をクリックすると、先ほど作成したレコードが表示されます。

image

ここで、レコード名(そのデータベースレコードのID)が 31CEB769-8DC1-48FB-69C3-10CFC69C43F6 であることを覚えておいてください。これは後で必要になります。

(追加ステップ) 新たなレコードタイプを作成してアイテムのコレクションを保持する

これは、私たちが今使っている実例の場合に有効な追加ステップです。この場合、あるアイテム列が含まれるようなデータタイプを新たに一つ作成します。これで、アイテムのコレクションを指し示すそのデータベースのレコードのIDをエンコードできます。さらに、弊社のアプリが一般公開された後でも、そのコレクションを後から更新することが可能になります。

コレクションデータタイプの作成:

image

上記と同じ手順で、このデータタイプにクエリインデックスを追加することも忘れないでください。

コレクション・レコードの作成:

上記のレコードIDを使用して、アイテムのコレクションを含むレコードを作成します。

image

このレコードの名前は 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の開発用データベースを本番環境にデプロイする必要があります。

image

通知: 開発環境を使用しているため、ここで加えた変更は (アプリストアに公開される) プロダクションアプリケーションでアクセス可能ではありません。プロダクション環境で記録 (Record) を作成する必要があります。

デバッグのオプション

CloudKit 環境をデバッグする方法はいくつかあります

ライブログ

CloudKit ダッシュボードで、一番上のドロップダウンメニューをクリックして Log を選択します

image

環境の切り替え

シミュレーターでもプロダクション環境への切り替えが可能です。

Core DataCloudKit と使う際にはよい方法ではないかもしれません。Core Data がローカルの Core Data のデータベース構造とCloudkit を同期することを妨げる可能性があるからです。

プロダクション環境を使うには、.entitlement ファイルを探し、以下のラインを加えます。

<key>com.apple.developer.icloud-container-environment</key>
<string>Production</string>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1つの UIViewController 内の複数の UITableView

この記事で説明すること

今日は、主に UITableView の使い方を見ていきます。

  • UITabeViewDelegateUITableViewDataSource クラスを作成して、現在のコードを単純化します
  • このカスタムクラスを使用して、2つ以上のテーブルビューを1つの UIViewController に埋め込みます。

通常の構造

通常、既存の UIViewControllerUITableView を追加する場合は、次のように行います。

@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 {
        //やること:テーブルビューセルを生成する
    }

}

UITableViewDelegateUITableViewDataSource にある関数を別のクラスに抽出することができます

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

プログラム実行時のスクリーンショットはこちら:

image

次のステップ

これで tableDataSource を再利用できるので、複数の UITableView を表示するのに書くコードを減らせます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む