- 投稿日:2020-05-19T16:42:26+09:00
【Swift】キーボードの上のボタンの設置方法
①UIViewで作る方法
画面サイズに応じてボタンの配置を変更する必要あり
class ViewController: UIViewController,UITextFieldDelegate { @IBOutlet var textField: UITextField! override func viewDidLoad() { super.viewDidLoad() textField.delegate = self // Make a bar on the keyboard. let keyboardBar = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 45)) keyboardBar.backgroundColor = UIColor.systemOrange // Make an enter button. let enter = UIButton(frame: CGRect(x: 280, y: 5, width: 80, height: 35)) enter.backgroundColor = UIColor.white enter.layer.cornerRadius = 17 enter.setTitle("決定", for: UIControl.State.normal) enter.setTitleColor(UIColor.black, for: UIControl.State.normal) enter.addTarget(self, action: #selector(tapEnter), for: UIControl.Event.touchUpInside) // Make a cancel button. let cancel = UIButton(frame: CGRect(x: 10, y: 5, width: 120, height: 35)) cancel.backgroundColor = UIColor.white cancel.layer.cornerRadius = 17 cancel.setTitle("キャンセル", for: UIControl.State.normal) cancel.setTitleColor(UIColor.black, for: UIControl.State.normal) cancel.addTarget(self, action: #selector(tapCancel), for: UIControl.Event.touchUpInside) // Set the buttons. keyboardBar.addSubview(enter) keyboardBar.addSubview(cancel) // Set the bar. textField.inputAccessoryView = keyboardBar } @objc func tapEnter(_ sender: UIButton){ // タップした時の処理 } @objc func tapCancel(_ sender: UIButton){ // タップした時の処理 } }完成形
②UIToolBarで作る方法
画面サイズに応じてキーボードの上のバーのサイズが変わる
class ViewController: UIViewController,UITextFieldDelegate { @IBOutlet var textField: UITextField! override func viewDidLoad() { super.viewDidLoad() textField.delegate = self let keyboardBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 40)) keyboardBar.barStyle = UIBarStyle.default keyboardBar.sizeToFit() let spacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil) let done = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done, target: self, action: #selector(self.done)) done.tintColor = .systemOrange let cancel = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel, target: self, action: #selector(self.cancel)) cancel.tintColor = .systemOrange keyboardBar.items = [cancel, spacer, done] textField.inputAccessoryView = keyboardBar } @objc func done(_ sender: UIButton){ // タップした時の処理 } @objc func cancel(_ sender: UIButton){ // タップした時の処理 } }完成形
- 投稿日:2020-05-19T13:04:51+09:00
画面遷移時にMain Thread Checkerのエラーが出た時の対処法(Swift)
はじめに
画面遷移などをする際に下記のようなエラーが出た時の対処法を紹介します。
エラー内容 "Modifications to the layout engine must not be performed from a background thread after it has been accessed from the main thread" "Main Thread Checker: UI API called on a background thread"対処法 (DispatchQueue.main)
DispatchQueue.main.sync {} もしくは DispatchQueue.main.async {} の中に対象のコードを入れる
同期処理の場合
DispatchQueue.main.sync { navigationController?.popViewController(animated: true) ←ここに画面遷移などのコードを記載 }
非同期処理の場合
DispatchQueue.main.async { navigationController?.popViewController(animated: true) ←ここに画面遷移などのコードを記載 }原因
UIの更新はメインスレッドで行う必要があるため、DispatchQueue.mainに処理を追加しないといけないとのこと。
https://1000ch.net/posts/2016/dispatch-queue.html
https://developer.apple.com/documentation/dispatch/dispatchqueue
- 投稿日:2020-05-19T09:27:31+09:00
Swift5.3の変更点まとめ
はじめに
Swift5.3がリリース最終段階に入ったらしいので、変更が入った機能についてまとめた。
変更点は下記ページのProposalを元に確認した。
https://apple.github.io/swift-evolution/#?version=5.3(理解が異なる箇所があればご指摘ください)
Swift5.3
リリースプロセス
スナップショット
変更サマリ
- 品質とパフォーマンスの強化
- Swiftが利用可能できるプラットフォームの拡張
- Windowsサポート
- 対応Linuxディストリビューションの追加
- 0263-string-uninitialized-initializer:初期化されていないバッファを使用するStringのイニシャライザを追加
- 0266-synthesized-comparable-for-enumerations:定義値のみでのenumのComparable準拠
- 0267-where-on-contextually-generic:関数定義に対するGenericsの条件指定
- 0268-didset-semantics:didSetでの不要なゲッター呼び出し見直し
- 0269-implicit-self-explicit-capture:不必要なselfの省略
- 0270-rangeset-and-collection-operations:非連続な位置を参照するコレクション操作の追加
- 0271-package-manager-resources :Swift Package Managerでのリソースファイル対応
- 0272-swiftpm-binary-dependencies:Swift Package Managerでのバイナリパッケージ利用対応
- 0273-swiftpm-conditional-target-dependencies:プラットフォームやConfigurationを条件にしたSPMターゲットの構成
- 0276-multi-pattern-catch-clauses:複数パターンcatch句のサポート
- 0277-float16:Float16の標準ライブラリ追加
- 0278-package-manager-localized-resources:Swift Package Managerリソースのローカライズ対応
- 0279-multiple-trailing-closures:複数TrailingClosure記述方法の改善
- 0280-enum-cases-as-protocol-witnesses:enumのプロトコル準拠判定の改善
- 0281-main-attribute:アプリのエントリーポイントを指定できるmain属性の追加
try! Swift5.3
Dockerのイメージが公式で提供されているので、簡易的にこれを使って試してみた。
https://hub.docker.com/_/swift/$ mkdir swift5.3; cd $_; $ touch main.swift # 好みのエディタで編集 $ docker pull swiftlang/swift:nightly-5.3-bionic $ docker run --rm --privileged -v $(pwd):/swift5.3 -it swiftlang/swift:nightly-5.3-bionic /bin/bash root@a526a6b578b6:/# swift swift5.3/main.swift[SE-0263] Add a String Initializer with Access to Uninitialized Storage
SE-0263:初期化されていないバッファを使用するStringのイニシャライザを追加
新しいイニシャライザを使うことで、UnsafeMutableBufferPointerを自前で取り扱わずに、初期化されていないバッファを使用したStringの生成ができるようになった。
// Swift5.3 let myCocoaString = NSString("The quick brown fox jumps over the lazy dog") as CFString var myString = String(unsafeUninitializedCapacity: CFStringGetMaximumSizeForEncoding(myCocoaString, …)) { buffer in var initializedCount = 0 CFStringGetBytes( myCocoaString, buffer, …, &initializedCount ) return initializedCount } // myString == "The quick brown fox jumps over the lazy dog"[SE-0266] Synthesized
Comparable
conformance forenum
typesSE-0266:定義値のみでのenumのComparable準拠
Swift5.2以前はenumの値同士を比較したい場合、Comparableに準拠させた上で、明示的に比較メソッドを実装しておく必要があった。
// Swift5.2 enum Size: Comparable { case small case medium case large private var comparisonValue: Int { switch self { case .small: return 0 case .medium: return 1 case .large: return 2 } } // この実装が必要 static func < (lhs: Size, rhs: Size) -> Bool { lhs.comparisonValue < rhs.comparisonValue } } Size.small < Size.medium // true Size.medium < Size.large // true Size.large < Size.small // false let sizes: [Size] = [.medium, .small, .large] sizes.sorted() // [Size.small, Size.medium, Size.large]Swift5.3からは該当メソッドを実装しなかった場合、定義順で比較されるようになり、後に定義された方が大きいとみなされるようになる。
// Swift5.3 enum Size: Comparable { case small case medium case large } let sizes: [Size] = [.medium, .small, .large] sizes.sorted() // [Size.small, Size.medium, Size.large]値付きenumの場合も対応可能で、この場合はassociatedValueもComparableに準拠していることが求められる。
同一のcaseだが値が異なるものの場合、比較はペイロードの値を元に行われる。// Swift5.3 enum Category: Comparable { case tops(Size) case bottoms(Size) } let categories: [Category] = [.bottoms(.medium), .tops(.large), .bottoms(.small), .tops(.medium)] categories.sorted() // [Category.tops(Size.medium), Category.tops(Size.large), Category.bottoms(Size.small), Category.bottoms(Size.medium)]所感
これを活用することでボイラープレートコードをより少なくできそう。
一方でカジュアルに定義の順番を変更しちゃったりするとバグる可能性があるので注意はした方がいい。[SE-0267]
where
clauses on contextually generic declarationsSE-0267:関数定義に対するGenericsの条件指定
下記のコード例に関して、「ElementがComparableに適合している場合のみsortedメソッドを実行できる」という挙動を実現したい場合、Swift5.2以前では対象クラスのextensionに対してwhere句で条件指定をする必要があった。
// Swift5.2 struct Stack<Element> { private var elements = [Element]() mutating func push(_ obj: Element) { elements.append(obj) } mutating func pop(_ obj: Element) -> Element? { elements.popLast() } } // extensionでGenericsに関する条件を指定する extension Stack where Element: Comparable { func sorted() -> [Element] { elements.sorted() } }これが、Swift5.3からは直接関数に対して
where句
でGenericsの条件指定を加えられるようにもなった。// Swift5.3 extension Stack { // extensionにではなく、関数にwhere句を付与できる func sorted() -> [Element] where Element: Comparable { elements.sorted() } }これにより、extensionで切り出さなくても直接class, struct内にGenericsの条件を指定した関数を定義することも可能になる。
// Swift5.3 struct Stack<Element> { private var elements = [Element]() mutating func push(_ obj: Element) { elements.append(obj) } mutating func pop(_ obj: Element) -> Element? { elements.popLast() } // extensionで切り出さなくても条件指定をした関数を宣言できる func sorted() -> [Element] where Element: Comparable { elements.sorted() } }所感
Extensionでは区切らない方が関数を意味的にグルーピングしやすい場合などに便利そう。
使い方によっては冗長性を排除できる気がするので、積極的に使っていきたい。[SE-0268] Refine
didSet
SemanticsSE-0268:didSetでの不要なゲッター呼び出し見直し
Swift5.2以前では、プロパティにdidSetを追加した場合、値に変更があると常にoldValueの値が内部的に参照されるようになっていた。
これにより、意図せずパフォーマンスを低下させてしまうコードを実装してしまう恐れがあった。// swift5.2 struct Container { var items: [Int] = .init(repeating: 1, count: 100) { didSet { // 実装としてoldValueを参照していなくても、内部的に参照されていた } } mutating func update() { (0..<items.count).forEach { items[$0] = $0 + 1 } } } var container = Container() container.update() // この時点でitemsのoldValueを参照するために100回配列のコピーが行われるSwift5.3ではこの挙動が改善され、didSetの内部でoldValueを明示的に参照しない限り、内部的にも参照されないようになった。
所感
oldValueが常に参照されていることを意識せずに実装していたので、こういった改善はありがたい。
また、もともとdidSet内でoldValueが呼び出されることに依存しているような処理があった場合は、挙動が変わってしまうことになるため注意が必要(そもそもそういった実装はよくないが)。[SE-0269] Increase availability of implicit
self
in@escaping
closures when reference cycles are unlikely to occurSE-0269:不必要なselfの省略
Swift5.2では循環参照の可能性がない場合でも、
@escaping
クロージャを受け渡す場合には必ずself.
を記述する必要があった。func someFunction(closure: @escaping () -> Void) { closure() }// Swift5.2 struct Main { func run() { someFunction { // 必ずself.で参照する必要がある self.helloWorld() } } func helloWorld() { print("Hello World") } } Main().run()これがSwift5.3では、循環参照の可能性がないと判断できる場合には
self.
を省略することができるようになった。selfが値型の場合は、循環参照の可能性がないため、
self.
をそのまま省略できる。// Swift5.3 // 値型 struct Main { func run() { someFunction { // selfが不要 helloWorld() } } }selfが参照型の場合は、
[self]
をつけるとselfを省略できるようになり、循環参照の可能性がある場合は従来通り[weak self]
を付与してselfを記述して利用する形になる。// Swift5.3 // 参照型 class Main { func run() { // [self]をつければself.が省略可能 someFunction { [self] in helloWorld() } } }つまり、
[weak self]
にするべきか[self]
にするべきか、一度決めてしまえばあとはself.
を書かなくてよくなるため、循環参照を意識させつつも不要なselfは書かなくてよくなることになる。所感
循環参照の可能性を強制的に考慮させる仕組みは残しつつ、不要なselfの記述も省略もできるのでとても良い。
self.の記述がたびたび必要になるSwiftUIのソースコードもきれいになりそう。[SE-0270] Add Collection Operations on Noncontiguous Elements
SE-0270:非連続な位置を参照するコレクション操作の追加
RangeSet
という配列内の非連続な値の位置を表す型が追加される。
また、これをベースに配列を操作するオペレーションも追加される。Collectionに
subranges
メソッドが追加されており、引数として条件のクロージャを渡すことでRangeSetオブジェクトが取得できる(返却されるのは値自体ではない)。// Swift5.3 var numbers = Array(1...6) let multipleOf2 = numbers.subranges(where: { $0.isMultiple(of: 2) }) // RangeSet(1..<2, 3..<4, 5..<6) let multipleOf3 = numbers.subranges(where: { $0.isMultiple(of: 3) }) // RangeSet(2..<3, 5..<6)RangeSetは集合を操作するメソッドも使える。
// 和集合 multipleOf2.union(multipleOf3) // RangeSet(1..<4, 5..<6) // 積集合 multipleOf2.intersection(multipleOf3) // RangeSet(5..<6)元の配列に対して、subscriptでRangeSetを渡すとスライス(DiscontiguousSlice) が取得できる。
これに対してメソッドを呼び出したり、値を取り出したりしてあげることで、対象のデータのみを取り扱うことも可能。let count = numbers[multipleOf2].count // 3 let total = numbers[multipleOf2].reduce(0, +) // 12 let values = multipleOf3.ranges.flatMap { numbers[$0] } // [3, 6] let array = Array(numbers[multipleOf3]) // [3, 6]また、RangeSetに含まれる要素をまとめて指定ポイントに移動する便利関数なども用意されている
numbers.moveSubranges(multipleOf2, to: numbers.endIndex) // 3..<6(移動先のRangeが返却される) numbers // [1, 3, 5, 2, 4, 6]所感
複雑な配列操作をしたいケースなど、これを使うと泥臭い実装をしなくて済む時がありそう。[SE-0271] Package Manager Resources
SE-0271 : Swift Package Managerでのリソースファイル対応
これまでSwift Package Managerでできなかった画像、オーディオ、Storyboard、JSONなどのリソースファイルをバンドルできるようになった。
targetにresourcesが追加されており、これにResourceのインスタンスを渡すことで設定する。
public static func target( name: String, dependencies: [Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, resources: [Resource]? = nil, // <=== NEW publicHeadersPath: String? = nil, cSettings: [CSetting]? = nil, cxxSettings: [CXXSetting]? = nil, swiftSettings: [SwiftSetting]? = nil, linkerSettings: [LinkerSetting]? = nil ) -> Target/// Represents an individual resource file. public struct Resource { /// Apply the platform-specific rule to the given path. /// /// Matching paths will be processed according to the platform for which this /// target is being built. For example, image files might get optimized when /// building for platforms that support such optimizations. /// /// By default, a file will be copied if there is no specialized processing /// for its file type. /// /// If path is a directory, the rule is applied recursively to each file in the /// directory. public static func process(_ path: String) -> Resource /// Apply the copy rule to the given path. /// /// Matching paths will be copied as-is and will be at the top-level /// in the bundle. The structure is retained for if path is a directory. public static func copy(_ path: String) -> Resource }SPMはBundleのextensionとして
module
を提供するので、これを使って実行時にリソースにアクセスできる。
(このextensionはモジュールごとinternalで提供されるので、各モジュールで実装が干渉することはない。)extension Bundle { static let module: Bundle = { ... }() }// DefaultSettings.plistへのパスを取得する let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist") // モジュールに含まれるリソースを利用して画像を取得する let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))所感
UI系のライブラリなどもをSPMで提供しやすくなってそう。[SE-0272] Package Manager Binary Dependencies
SE-0272:Swift Package Managerでのバイナリパッケージ利用対応
Swift Package Managerでソースコード自体は公開せずにパッケージを提供できるようになる。
これにより、FirebaseSDKなどのクローズドSDKがSPMに対応できるようになる。※ この機能は、最初のフェーズではAppleのプラットフォームに限定し、将来的に拡張しておく模様。
let package = Package( name: "SomePackage", platforms: [ .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2), ], products: [ .library(name: "SomePackage", targets: ["SomePackageLib"]) ], targets: [ .binaryTarget( name: "SomePackageLib", url: "https://github.com/some/package/releases/download/1.0.0/SomePackage-1.0.0.zip", checksum: "839F9F30DC13C30795666DD8F6FB77DD0E097B83D06954073E34FE5154481F7A" ), .binaryTarget( name: "SomeLibOnDisk", path: "artifacts/SomeLibOnDisk.zip" ) ] )ソースターゲットとバイナリターゲットを混在させることもできる。
所感
この流れでCocoapods, Carthageで管理しているライブラリもSPMに移行できたら嬉しい。[SE-0273] Package Manager Conditional Target Dependencies
SE-0273:プラットフォームやConfigurationを条件にしたSPMターゲットの構成
以下のように、条件ごとに設定を変えるような対応ができるようになる
- macOS向けとLinux向けでフレームワークを使い分ける
- debugビルドの時のみ追加のフレームワークを組み込む
// swift-tools-version:5.3 import PackageDescription let package = Package( name: "BestPackage", dependencies: [ .package(url: "https://github.com/pureswift/bluetooth", .branch("master")), .package(url: "https://github.com/pureswift/bluetoothlinux", .branch("master")), ], targets: [ .target( name: "BestExecutable", dependencies: [ .product(name: "Bluetooth", condition: .when(platforms: [.macOS])), .product(name: "BluetoothLinux", condition: .when(platforms: [.linux])), .target(name: "DebugHelpers", condition: .when(configuration: .debug)), ] ), .target(name: "DebugHelpers") ] )所感
1つのパッケージでより汎用的に使えるライブラリが提供できて便利そう。[SE-0276] Multi-Pattern Catch Clauses
SE-0276:複数パターンcatch句のサポート
Swift5.2では例外ハンドリング時のcatch句には、単一のパターンとwhere句のみしか設定できなかった。
そのため、各エラー発生時に同一の処理を行いたい場合にも、処理を重複して記述する必要があった。// Swift5.2 enum SampleError: Error { case badRequest(String) case unAuthorized(String) case internalServerError(String) case serviceUnavailable(String) } do { try doSomething() } catch SampleError.badRequest(let message) { handleClientError(with: message) } catch SampleError.unAuthorized(let message) { handleClientError(with: message) } catch SampleError.internalServerError(let message) { handleServerError(with: message) } catch SampleError.serviceUnavailable(let message) { handleServerError(with: message) }Swift5.3ではcatch句の文法が拡張され、パターンをカンマで区切ることでエラーハンドリングの共通化ができるようになった。
// Swift5.3 do { try doSomething() } catch SampleError.badRequest(let message), SampleError.unAuthorized(let message) { handleClientError(with: message) } catch SampleError.internalServerError(let message), SampleError.serviceUnavailable(let message) { handleServerError(with: message) }[SE-0277] Float16
SE-0277:Float16の標準ライブラリ追加
所感
グラフィックスプログラミングと機械学習で一般的に使用されている型のようなので、界隈のひとには嬉しい変更かもしれない。[SE-0278] Package Manager Localized Resources
SE-0278:SPMリソースのローカライズ対応
ローカライズされたリソースを、
<IFTF言語タグ>.lproj
ディレクトリに配置することで、ロケーションに応じたリソースの使い分けができるようになる。例として、以下のようなローカライズされたリソースが存在する場合、Package.swiftで指定する際は
<IFTF言語タグ>.lproj
を除いたパスで指定する。
- Resources/en.lproj/Icon.png
- Resources/fr.lproj/Icon.png
let package = Package( name: "BestPackage", defaultLocalization: "en", targets: [ .target(name: "BestTarget", resources: [ .process("Resources/Icon.png"), ]) ] )またSPMはApple固有プラットフォームのリソース対応のために、
Base.lproj
配下のリソースもBaseInternationalization
を使用するものとして認識する。[SE-0279] Multiple Trailing Closures
SE-0279:複数TrailingClosure記述方法の改善
Swiftでは、末尾の引数にクロージャを渡すコードを、明快で読みやすくできるTrailing Closureという機能が搭載されている。
// Swift5.2 // Trailing Closureを使わない UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }) // Trailing Closureを使うと、末尾のクロージャのラベルを省略した上で記述を外に出すことができる UIView.animate(withDuration: 0.3) { self.view.alpha = 0 }しかし、もし末尾に連続でクロージャが渡されると、それらが何を表すものかが不明瞭になり、ネストも深くなってしまう課題があった。
// Swift5.2 // Trailing Closureを使わない UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }, completion: { _ in self.view.removeFromSuperview() }) // Trailing Closureを使う UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }) { _ in // このクロージャが何なのかが不明瞭 self.view.removeFromSuperview() }そのため、Swift5.2以前では以下のように使い分ける対応をすることが多かった。
- 末尾のクロージャが単一 → Trailing Closureを利用する
- 末尾のクロージャが連続 → Trailing Closureを利用しない
Swift5.3ではこの記述方法に改善が入り、1つめのクロージャには既存と同様の省略記法を適用し、後続するクロージャはラベルを付与した上で表現するといった記述が可能になった。(これまでと同様の記述方法も可能)
// Swift 5.3 UIView.animate(withDuration: 0.3) { self.view.alpha = 0 } completion: { _ in self.view.removeFromSuperview() }所感
関数の定義方法を工夫することで読みやすいコードにできそうな気がする。
メソッド名に対して関連度が高いクロージャを最初に指定して、後続するクロージャは追加の意味合いが強いものを持ってくると読みやすくなりそう。// Swift5.3 func listen(onReceive: () -> Void, onCancel: () -> Void) {} listen { print("onReceive") } onCancel: { print("onCancel") }[SE-0280] Enum cases as protocol witnesses
SE-0280:enumのプロトコル準拠判定の改善
下記のJSONDecodingError定義は、struct, enumでそれぞれ全く同じように呼び出すことができる。
struct JSONDecodingError { static var fileCorrupted: Self {} static func keyNotFound(_ key: String) -> Self {} } enum JSONDecodingError { case fileCorrupted case keyNotFound(_ key: String) } let error1 = JSONDecodingError.fileCorrupted let error2 = JSONDecodingError.keyNotFound("hoge")しかし、これを下記のDecodingErrorプロトコルに準拠させようとすると、Swift5.2の場合ではstructで定義した場合のみプロトコルに準拠していると見なされ、enumの場合は不適合と見なされてしまっていた。
// Swift5.2 protocol DecodingError { static var fileCorrupted: Self { get } static func keyNotFound(_ key: String) -> Self } // o struct JSONDecodingError: DecodingError { ... } // x enum JSONDecodingError: DecodingError { ... }Swift5.3ではこれに改善が入り、以下のルールに合致した場合、プロトコルに適合していると見なされるようになった。
- 値を持たないenumのケースは、列挙型もしくはSelfを保持する静的なget-onlyプロパティと関連付けられる
- 値を持つenumのケースは、associatedValueと同じ値を引数に取り、列挙型もしくはSelfを返す静的関数と関連付けられる
// Swift5.3 protocol DecodingError { static var fileCorrupted: Self { get } static func keyNotFound(_ key: String) -> Self } // OK enum JSONDecodingError: DecodingError { case fileCorrupted case keyNotFound(_ key: String) }[SE-0280]
@main
: Type-Based Program Entry PointsSE-0281:アプリのエントリーポイントを指定できる@main属性の追加
Swiftはmain.swiftを自動的にトップレベルのコードとみなすため、Swift5.2以前では以下のような実装をすることでエントリーポイントを定義していた。
main.swift// Swift5.2 struct MyApp { func run() { print("Running!") } } let app = MyApp() app.run()Swift5.3では、main.swiftを追加していない場合、
@main
属性が付与されたクラスのstatic main関数がエントリーポイントと見なされるようになる。
これはモジュールの任意のソースファイルに付与することができる。app.swift// Swift5.3 @main struct MyApp { static func main() { print("Running!") } }
@main
属性を使用するには下記の制限がある。
- main.swiftが存在するアプリでは
@main
属性は使用できない(既存と同様の動作)- 複数の
@main
属性は付与できない所感
ライブラリの実装者などは、protocol extensionなどでstatic main関数をあらかじめ定義しておけば、利用者側のエントリーポイントに該当のプロトコルの準拠+@main属性の付与をさせるだけで、望んだ動作をさせられるのは良さそう。参考
- 投稿日:2020-05-19T09:08:36+09:00
こんなソースコードはイヤだ-グローバルな関数の使いどころ
- 投稿日:2020-05-19T08:56:49+09:00
【Swift】サブスクリプトを公式ドキュメントを元にまとめてみた
サブスクリプトとは??
クラス構造体列挙体は、コレクション、リストまたはシーケンスのメンバー要素にアクセスするためのショートカットである添え字を定義できます。
つまりサブスクリプトとは型が配列等の要素にアクセスするためのショートカットのこと!
何がメリットなのか
使用することによって、値の取得・設定をしたい時にわざわざメソッドを用いる必要がなくなり、サブスクリプトのインデックスの値によって行えることです!
構文
クラス・構造体・列挙体の中で、
subscript(引数)->戻り値{return ~~}
という構文で記述します。下のコード例では、
threeTimesTable[6]
により、TimesTable内のsubscriptのindex
に6が代入され、戻り値を返します。Syntaxstruct TimesTable{ let multiplier: Int subscript(index: Int) -> Int{ //indexには6がはいる return multiplier * index //read-only } } let threeTimesTable = TimesTable(multiplier: 3) print("six times three is \(threeTimesTable[6])") //threeTimesTable[6]がサブスク!サブスクの特徴1「複数のパラメーターを持てる」
サブスクリプトは、複数のパラメータを持つことができます!
下のコード例では、
subscript(row:Int, column:Int)
とあるように2つのパラメータを持ってます。multipleSubscripstruct Matrix{ let rows: Int, columns: Int var grid: [Double] init(rows:Int, columns:Int){ self.rows = rows self.columns = columns grid = Array(repeating: 0.0, count: rows * columns) } func indexIsValid(row: Int, column:Int) -> Bool { return row >= 0 && row < rows && column >= 0 && column < columns } subscript(row:Int, column:Int) -> Double{ get{ assert(indexIsValid(row: row, column: column), "Index out of range") return grid[(row * columns) + column] } set{ assert(indexIsValid(row: row, column: column), "Index out of range") grid[(row * columns) + column] = newValue //サブスクで渡された値を元にgrid[]を更新 } } } var matrix = Matrix(rows: 2, columns: 2) print(matrix) //サブスク前の出力:Matrix(rows: 2, columns: 2, grid: [0.0, 0.0, 0.0, 0.0]) matrix[0,1] = 1.5 //サブスク matrix[1,0] = 2.5 //サブスク print(matrix) //サブスク後の出力:Matrix(rows: 2, columns: 2, grid: [0.0, 1.5, 2.5, 0.0])ちなみに、
assert(indexIsValid(row: row, column: column), "Index out of range")
については、初めてみたのでここに軽くassertについてメモしときます。assert → 条件をチェックしfalseの場合はエラーとなる 構文:assert(条件式(Bool),"エラーメッセージ")サブスクの特徴2「タイプに直接紐づけられる」
タイププロパティのように、タイプに直接紐づけることができます。
これまで紹介してきたサブスクは全てインスタンスサブスクです。
インスタンスに対して添え字を用いてますね。
threeTimesTable[6]
matrix[0,1] = 1.5
タイプサブスクリプトは、その名の通り、
staticキーワード
を用いることでタイプ[0]
のようにタイプに直接サブスクを使えます!TypeSubscriptenum Planet: Int { case mercury = 1, vanus, earth, mars, jupiter, saturn, uranus, neptune static subscript(n: Int) -> Planet{ return Planet(rawValue: n)! } } let mars = Planet[4] print(mars)
- 投稿日:2020-05-19T07:40:54+09:00
iOSアプリのメモリリーク (Memory Leak) の検出とデバッグの方法を、ネコに関するプログラムで学びましょう。(Strong Reference 強く参照)
iOSアプリのメモリリークの検出とデバッグの方法を、ネコに関するプログラムで学びましょう!
今日は、サンプルプログラムを使って、iOSアプリにおけるメモリリークとは何なのかを学んでいきます。「僕のイタズラ猫)」というプログラムで、メモリリークの直し方を学びます。
はじめに、Xcodeに実装されているデバッグツールを使ってメモリーの問題を探し出す方法を学びます。次に、実際にデバッグを行います。
サンプルプログラム
私はたくさんのネコを飼っています。たまにいずれかのネコがソファーを引っ掻いてしまいます。どのネコが一番「行儀が悪い」かを確認するためにこのリストを作りました。
(冗談だよ。 私は猫が大好きです。)
Githubでサンプルプログラムをダウンロードしてください:: https://github.com/mszopensource/MemoryLeakTesting
今回使うサンプルプログラムは、猫の「イタズラ度」のテーブルです。上下のボタンを押せば猫のランクを変えられます!開始をクリックすると、猫の名前の一覧のテーブルが表示されます。表示されたテーブル上では、セル1つに対してボタンが2つ付いていて、1つは猫のランクを上げるボタン、もう1つは下げるボタンです。
UITableView (catList)
-> UITableViewCells (naughtyCell)class catList: UITableViewController { var myCats = ["ネコノヒー", "ムギ", "レオ", "ソラ", "マル"] override func viewDidLoad() { super.viewDidLoad() title = "Best Cats!" } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return myCats.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell let catName = myCats[indexPath.row] cell.catNameLabel.text = catName cell.catTableView = self cell.catID = indexPath.row return cell } func moveCatUp(forCatNumber: Int) { if forCatNumber == 0 { //最高にお利口さんのネコがこちらです。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。 return } myCats.swapAt(forCatNumber, forCatNumber - 1) tableView.reloadData() } func moveCatDown(forCatNumber: Int) { if forCatNumber == (myCats.count - 1) { //宇宙で一番行儀が悪いネコ。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。 return } myCats.swapAt(forCatNumber, forCatNumber + 1) //ネコ「forCatNumber + 1」がもっと行儀悪くなりました!!! tableView.reloadData() } }class naughtyCell: UITableViewCell { @IBOutlet weak var catNameLabel: UILabel! /* もちろん、ネコの行儀の悪さは変化します。 - 2つのボタンを設けてこれに対応します。 -- 行儀悪さのランクをアップ -- 行儀悪さのランクをダウン */ @IBAction func actionMoveUp(){ catTableView.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ catTableView.moveCatDown(forCatNumber: catID) } /* そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。 */ var catTableView: catList! /* また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります... */ var catID: Int! }メモリ問題の発見
Xcode 内には、私たちのプログラムのデバッグとパフォーマンスの測定を行うための優れたツールが数多く存在します。
このプログラムを実行したとき、Xcode のウィンドウの左にあるこの小さなアイコンをクリックするとプログラムのマトリックスを見ることができます。
ここで「Memory」セクションをクリックして、私たちのアプリがメモリをどのくらい使用しているのかを見てみましょう。
今、下部に表示されているメモリグラフは横ばいの状態です。なぜなら、アプリ内でまだ何もアクションを実行していないからです。
ではアプリのインターフェースの「開始」ボタンをクリックしてみましょう。メモリの曲線が上向きに変化したでしょうか?
これはアプリがもう1つの「UIViewController」をスクリーン上にロードしたからであり、普通のことです。それはこれだけのスペースを使用します。
ですが覚えておいてください。もし何らかのプログラムがメモリの一部を借りてきた場合、使用を終えたらそのメモリを返すはずですよね?では「UIViewController」を閉じて、メモリ使用率が下がるかどうか確認してみましょう。
確かに下がりましたが、その下がり幅はわずかです。なぜでしょうか?
では、問題をよりわかりやすくするためにそのビューの開く、閉じるを何度も繰り返してみてましょう。ビューを10回開いてみて、どうなるか確認してみてください:
これで分かりましたね。私たちのアプリはメモリに問題を抱えています!UIViewControllerを閉じても、一部のメモリが開放されません。どうすればいいのでしょうか?
ではここで、Xcodeの2つめのツールをご紹介しましょう。メモリ確保ビューです。このツールを使うことで、メモリ内に現在何のコンポーネントがあるのかを確認できます。
Xcode ウィンドウの下部にこのボタンがあります。
詳しく見てみましょう。
このボタンをクリックするとアプリが一時停止し、Xcodeがメモリ内に何のコンポーネントが残っているかをチェックします。
ここで面白いものが見つかりました。現在メモリ内にあるコンポーネントです。
ここで何が起きたか見てください!私たちが開いた全ての「catList」がスマートフォンのメモリ内に残っています。そして、70個の「naughtyCell」があります。これらが開放されていないのは明らかです。
メモリバックトレース
このメモリツールのもう一つの優れた点は、メモリリークを引き起こしたコード内の行を表示できることです。ただし、それを行うためには、新しいメモリロギングを有効にする必要があります。
最初に、一番上にあるプログラムの名前をクリックします:
次に、「Edit Scheme スキームを編集」を選択します:
「Diagnostics 診断」タブをクリックします
クリックして「Malloc Stackスタック」を有効にします
今度は、当社のアプリを再実行して、前回取ったのと同じアクション(「catList」を10回開閉する)を実行し、メモリデバッグアイコンをクリックします。そして「naughtyCell」のいずれかをクリックします:
それから画面の右側にリークの場所が表示されるようになります:
現在、まだこの問題を解決する必要があります。
メモリリークの修正
Xcodeに含まれているツールは、メモリリークの発生箇所を検出し、表示してくれるので、非常に便利でした。しかし、この種の問題を修正するには、手作業の調査が必要です。
メモリリークのよくある原因の一つは「参照サイクル」と呼ばれれるものです。強く参照されている2つのオブジェクトが互いを参照し合うと参照サイクルが発生します。
また、どちらのオブジェクトも強く参照されているため、システムはいずれのオブジェクトもメモリから削除できません。したがって、これらのオブジェクトは永久にメモリに保存されます。
それでは、「naughtyCell」の実装を見てみましょう。
class naughtyCell: UITableViewCell { @IBOutlet weak var catNameLabel: UILabel! /* もちろん、ネコの行儀の悪さは変化します。 - 2つのボタンを設けてこれに対応します。 -- 行儀悪さのランクをアップ -- 行儀悪さのランクをダウン */ @IBAction func actionMoveUp(){ catTableView.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ catTableView.moveCatDown(forCatNumber: catID) } /* そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。 */ var catTableView: catList! /* また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります... */ var catID: Int! }強い参照 (Strong Reference) に気付きましたか?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell let catName = myCats[indexPath.row] cell.catNameLabel.text = catName cell.catTableView = self cell.catID = indexPath.row return cell }これは「naughtyCell」が「catList」を参照している例です。
var catTableView: catList!これは「catList」が「naughtyCell」を参照している例です。
cell.catTableView = selfこれらの2つのコンポーネントは互いを参照しています。:
さて、私たちは「catTableView.moveCatUp(forCatNumber: catID)」と「catTableView.moveCatDown(forCatNumber: catID)」を呼び出す必要があるため、「naughtyCell」をそれぞれの「naughtyCell」に保存する必要があることを思い出してください。解決策は?「プロトコルデリゲート」を使うことです。
デリゲート (Delegate / Protocol) は、2つのプログラミングクラス間で情報を渡すメディアのようなものです。2つの関数を持つデリゲートを作成できます:
protocol catListActionDelegate: AnyObject { func moveCatUp(forCatNumber: Int) func moveCatDown(forCatNumber: Int) }次に、型「catListActionDelegate」を持つ変数を「naughtyCell」に導入し、代わりにそのデリゲート内の関数を呼び出します。
weak var delegate: catListActionDelegate?そして、ユーザーがアクションを実行したときに、デリゲート内の関数を呼び出します。
@IBAction func actionMoveUp(){ delegate?.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ delegate?.moveCatDown(forCatNumber: catID) }catList(UITableView)で、デリゲートを実装します:
class fixedCatList: UITableViewController, catListActionDelegateそして、必要とされるデリゲート関数を実装する必要があります。デリゲート内の関数名が「catList」内の既存の関数名と一致するため、ここでは何もする必要はありません。
新しいメモリグラフ
- 投稿日:2020-05-19T07:40:54+09:00
iOSアプリのメモリリーク (Memory Leak) の検出とデバッグの方法を、ネコに関するプログラムで学びましょう。(Reference Cycle)
iOSアプリのメモリリークの検出とデバッグの方法を、ネコに関するプログラムで学びましょう!
今日は、サンプルプログラムを使って、iOSアプリにおけるメモリリークとは何なのかを学んでいきます。「僕のイタズラ猫)」というプログラムで、メモリリークの直し方を学びます。
はじめに、Xcodeに実装されているデバッグツールを使ってメモリーの問題を探し出す方法を学びます。次に、実際にデバッグを行います。
サンプルプログラム
私はたくさんのネコを飼っています。たまにいずれかのネコがソファーを引っ掻いてしまいます。どのネコが一番「行儀が悪い」かを確認するためにこのリストを作りました。
(冗談だよ。 私は猫が大好きです。)
Githubでサンプルプログラムをダウンロードしてください:: https://github.com/mszopensource/MemoryLeakTesting
今回使うサンプルプログラムは、猫の「イタズラ度」のテーブルです。上下のボタンを押せば猫のランクを変えられます!開始をクリックすると、猫の名前の一覧のテーブルが表示されます。表示されたテーブル上では、セル1つに対してボタンが2つ付いていて、1つは猫のランクを上げるボタン、もう1つは下げるボタンです。
UITableView (catList)
-> UITableViewCells (naughtyCell)class catList: UITableViewController { var myCats = ["ネコノヒー", "ムギ", "レオ", "ソラ", "マル"] override func viewDidLoad() { super.viewDidLoad() title = "Best Cats!" } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return myCats.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell let catName = myCats[indexPath.row] cell.catNameLabel.text = catName cell.catTableView = self cell.catID = indexPath.row return cell } func moveCatUp(forCatNumber: Int) { if forCatNumber == 0 { //最高にお利口さんのネコがこちらです。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。 return } myCats.swapAt(forCatNumber, forCatNumber - 1) tableView.reloadData() } func moveCatDown(forCatNumber: Int) { if forCatNumber == (myCats.count - 1) { //宇宙で一番行儀が悪いネコ。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。 return } myCats.swapAt(forCatNumber, forCatNumber + 1) //ネコ「forCatNumber + 1」がもっと行儀悪くなりました!!! tableView.reloadData() } }class naughtyCell: UITableViewCell { @IBOutlet weak var catNameLabel: UILabel! /* もちろん、ネコの行儀の悪さは変化します。 - 2つのボタンを設けてこれに対応します。 -- 行儀悪さのランクをアップ -- 行儀悪さのランクをダウン */ @IBAction func actionMoveUp(){ catTableView.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ catTableView.moveCatDown(forCatNumber: catID) } /* そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。 */ var catTableView: catList! /* また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります... */ var catID: Int! }メモリ問題の発見
Xcode 内には、私たちのプログラムのデバッグとパフォーマンスの測定を行うための優れたツールが数多く存在します。
このプログラムを実行したとき、Xcode のウィンドウの左にあるこの小さなアイコンをクリックするとプログラムのマトリックスを見ることができます。
ここで「Memory」セクションをクリックして、私たちのアプリがメモリをどのくらい使用しているのかを見てみましょう。
今、下部に表示されているメモリグラフは横ばいの状態です。なぜなら、アプリ内でまだ何もアクションを実行していないからです。
ではアプリのインターフェースの「開始」ボタンをクリックしてみましょう。メモリの曲線が上向きに変化したでしょうか?
これはアプリがもう1つの「UIViewController」をスクリーン上にロードしたからであり、普通のことです。それはこれだけのスペースを使用します。
ですが覚えておいてください。もし何らかのプログラムがメモリの一部を借りてきた場合、使用を終えたらそのメモリを返すはずですよね?では「UIViewController」を閉じて、メモリ使用率が下がるかどうか確認してみましょう。
確かに下がりましたが、その下がり幅はわずかです。なぜでしょうか?
では、問題をよりわかりやすくするためにそのビューの開く、閉じるを何度も繰り返してみてましょう。ビューを10回開いてみて、どうなるか確認してみてください:
これで分かりましたね。私たちのアプリはメモリに問題を抱えています!UIViewControllerを閉じても、一部のメモリが開放されません。どうすればいいのでしょうか?
ではここで、Xcodeの2つめのツールをご紹介しましょう。メモリ確保ビューです。このツールを使うことで、メモリ内に現在何のコンポーネントがあるのかを確認できます。
Xcode ウィンドウの下部にこのボタンがあります。
詳しく見てみましょう。
このボタンをクリックするとアプリが一時停止し、Xcodeがメモリ内に何のコンポーネントが残っているかをチェックします。
ここで面白いものが見つかりました。現在メモリ内にあるコンポーネントです。
ここで何が起きたか見てください!私たちが開いた全ての「catList」がスマートフォンのメモリ内に残っています。そして、70個の「naughtyCell」があります。これらが開放されていないのは明らかです。
メモリバックトレース
このメモリツールのもう一つの優れた点は、メモリリークを引き起こしたコード内の行を表示できることです。ただし、それを行うためには、新しいメモリロギングを有効にする必要があります。
最初に、一番上にあるプログラムの名前をクリックします:
次に、「Edit Scheme スキームを編集」を選択します:
「Diagnostics 診断」タブをクリックします
クリックして「Malloc Stackスタック」を有効にします
今度は、当社のアプリを再実行して、前回取ったのと同じアクション(「catList」を10回開閉する)を実行し、メモリデバッグアイコンをクリックします。そして「naughtyCell」のいずれかをクリックします:
それから画面の右側にリークの場所が表示されるようになります:
現在、まだこの問題を解決する必要があります。
メモリリークの修正
Xcodeに含まれているツールは、メモリリークの発生箇所を検出し、表示してくれるので、非常に便利でした。しかし、この種の問題を修正するには、手作業の調査が必要です。
メモリリークのよくある原因の一つは「参照サイクル」と呼ばれれるものです。強く参照されている2つのオブジェクトが互いを参照し合うと参照サイクルが発生します。
また、どちらのオブジェクトも強く参照されているため、システムはいずれのオブジェクトもメモリから削除できません。したがって、これらのオブジェクトは永久にメモリに保存されます。
それでは、「naughtyCell」の実装を見てみましょう。
class naughtyCell: UITableViewCell { @IBOutlet weak var catNameLabel: UILabel! /* もちろん、ネコの行儀の悪さは変化します。 - 2つのボタンを設けてこれに対応します。 -- 行儀悪さのランクをアップ -- 行儀悪さのランクをダウン */ @IBAction func actionMoveUp(){ catTableView.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ catTableView.moveCatDown(forCatNumber: catID) } /* そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。 */ var catTableView: catList! /* また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります... */ var catID: Int! }強い参照 (Strong Reference) に気付きましたか?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell let catName = myCats[indexPath.row] cell.catNameLabel.text = catName cell.catTableView = self cell.catID = indexPath.row return cell }これは「naughtyCell」が「catList」を参照している例です。
var catTableView: catList!これは「catList」が「naughtyCell」を参照している例です。
cell.catTableView = selfこれらの2つのコンポーネントは互いを参照しています。:
さて、私たちは「catTableView.moveCatUp(forCatNumber: catID)」と「catTableView.moveCatDown(forCatNumber: catID)」を呼び出す必要があるため、「naughtyCell」をそれぞれの「naughtyCell」に保存する必要があることを思い出してください。解決策は?「プロトコルデリゲート」を使うことです。
デリゲート (Delegate / Protocol) は、2つのプログラミングクラス間で情報を渡すメディアのようなものです。2つの関数を持つデリゲートを作成できます:
protocol catListActionDelegate: AnyObject { func moveCatUp(forCatNumber: Int) func moveCatDown(forCatNumber: Int) }次に、型「catListActionDelegate」を持つ変数を「naughtyCell」に導入し、代わりにそのデリゲート内の関数を呼び出します。
weak var delegate: catListActionDelegate?そして、ユーザーがアクションを実行したときに、デリゲート内の関数を呼び出します。
@IBAction func actionMoveUp(){ delegate?.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ delegate?.moveCatDown(forCatNumber: catID) }catList(UITableView)で、デリゲートを実装します:
class fixedCatList: UITableViewController, catListActionDelegateそして、必要とされるデリゲート関数を実装する必要があります。デリゲート内の関数名が「catList」内の既存の関数名と一致するため、ここでは何もする必要はありません。
新しいメモリグラフ
- 投稿日:2020-05-19T07:40:54+09:00
iOSアプリのメモリリーク (Memory Leak) の検出とデバッグの方法を、ネコに関するプログラムで学びましょう。(Reference Cycle 参照サイクル)
iOSアプリのメモリリークの検出とデバッグの方法を、ネコに関するプログラムで学びましょう!
今日は、サンプルプログラムを使って、iOSアプリにおけるメモリリークとは何なのかを学んでいきます。「僕のイタズラ猫)」というプログラムで、メモリリークの直し方を学びます。
はじめに、Xcodeに実装されているデバッグツールを使ってメモリーの問題を探し出す方法を学びます。次に、実際にデバッグを行います。
サンプルプログラム
私はたくさんのネコを飼っています。たまにいずれかのネコがソファーを引っ掻いてしまいます。どのネコが一番「行儀が悪い」かを確認するためにこのリストを作りました。
(冗談だよ。 私は猫が大好きです。)
Githubでサンプルプログラムをダウンロードしてください:: https://github.com/mszopensource/MemoryLeakTesting
今回使うサンプルプログラムは、猫の「イタズラ度」のテーブルです。上下のボタンを押せば猫のランクを変えられます!開始をクリックすると、猫の名前の一覧のテーブルが表示されます。表示されたテーブル上では、セル1つに対してボタンが2つ付いていて、1つは猫のランクを上げるボタン、もう1つは下げるボタンです。
UITableView (catList)
-> UITableViewCells (naughtyCell)class catList: UITableViewController { var myCats = ["ネコノヒー", "ムギ", "レオ", "ソラ", "マル"] override func viewDidLoad() { super.viewDidLoad() title = "Best Cats!" } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return myCats.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell let catName = myCats[indexPath.row] cell.catNameLabel.text = catName cell.catTableView = self cell.catID = indexPath.row return cell } func moveCatUp(forCatNumber: Int) { if forCatNumber == 0 { //最高にお利口さんのネコがこちらです。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。 return } myCats.swapAt(forCatNumber, forCatNumber - 1) tableView.reloadData() } func moveCatDown(forCatNumber: Int) { if forCatNumber == (myCats.count - 1) { //宇宙で一番行儀が悪いネコ。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。 return } myCats.swapAt(forCatNumber, forCatNumber + 1) //ネコ「forCatNumber + 1」がもっと行儀悪くなりました!!! tableView.reloadData() } }class naughtyCell: UITableViewCell { @IBOutlet weak var catNameLabel: UILabel! /* もちろん、ネコの行儀の悪さは変化します。 - 2つのボタンを設けてこれに対応します。 -- 行儀悪さのランクをアップ -- 行儀悪さのランクをダウン */ @IBAction func actionMoveUp(){ catTableView.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ catTableView.moveCatDown(forCatNumber: catID) } /* そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。 */ var catTableView: catList! /* また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります... */ var catID: Int! }メモリ問題の発見
Xcode 内には、私たちのプログラムのデバッグとパフォーマンスの測定を行うための優れたツールが数多く存在します。
このプログラムを実行したとき、Xcode のウィンドウの左にあるこの小さなアイコンをクリックするとプログラムのマトリックスを見ることができます。
ここで「Memory」セクションをクリックして、私たちのアプリがメモリをどのくらい使用しているのかを見てみましょう。
今、下部に表示されているメモリグラフは横ばいの状態です。なぜなら、アプリ内でまだ何もアクションを実行していないからです。
ではアプリのインターフェースの「開始」ボタンをクリックしてみましょう。メモリの曲線が上向きに変化したでしょうか?
これはアプリがもう1つの「UIViewController」をスクリーン上にロードしたからであり、普通のことです。それはこれだけのスペースを使用します。
ですが覚えておいてください。もし何らかのプログラムがメモリの一部を借りてきた場合、使用を終えたらそのメモリを返すはずですよね?では「UIViewController」を閉じて、メモリ使用率が下がるかどうか確認してみましょう。
確かに下がりましたが、その下がり幅はわずかです。なぜでしょうか?
では、問題をよりわかりやすくするためにそのビューの開く、閉じるを何度も繰り返してみてましょう。ビューを10回開いてみて、どうなるか確認してみてください:
これで分かりましたね。私たちのアプリはメモリに問題を抱えています!UIViewControllerを閉じても、一部のメモリが開放されません。どうすればいいのでしょうか?
ではここで、Xcodeの2つめのツールをご紹介しましょう。メモリ確保ビューです。このツールを使うことで、メモリ内に現在何のコンポーネントがあるのかを確認できます。
Xcode ウィンドウの下部にこのボタンがあります。
詳しく見てみましょう。
このボタンをクリックするとアプリが一時停止し、Xcodeがメモリ内に何のコンポーネントが残っているかをチェックします。
ここで面白いものが見つかりました。現在メモリ内にあるコンポーネントです。
ここで何が起きたか見てください!私たちが開いた全ての「catList」がスマートフォンのメモリ内に残っています。そして、70個の「naughtyCell」があります。これらが開放されていないのは明らかです。
メモリバックトレース
このメモリツールのもう一つの優れた点は、メモリリークを引き起こしたコード内の行を表示できることです。ただし、それを行うためには、新しいメモリロギングを有効にする必要があります。
最初に、一番上にあるプログラムの名前をクリックします:
次に、「Edit Scheme スキームを編集」を選択します:
「Diagnostics 診断」タブをクリックします
クリックして「Malloc Stackスタック」を有効にします
今度は、当社のアプリを再実行して、前回取ったのと同じアクション(「catList」を10回開閉する)を実行し、メモリデバッグアイコンをクリックします。そして「naughtyCell」のいずれかをクリックします:
それから画面の右側にリークの場所が表示されるようになります:
現在、まだこの問題を解決する必要があります。
メモリリークの修正
Xcodeに含まれているツールは、メモリリークの発生箇所を検出し、表示してくれるので、非常に便利でした。しかし、この種の問題を修正するには、手作業の調査が必要です。
メモリリークのよくある原因の一つは「参照サイクル」と呼ばれれるものです。強く参照されている2つのオブジェクトが互いを参照し合うと参照サイクルが発生します。
また、どちらのオブジェクトも強く参照されているため、システムはいずれのオブジェクトもメモリから削除できません。したがって、これらのオブジェクトは永久にメモリに保存されます。
それでは、「naughtyCell」の実装を見てみましょう。
class naughtyCell: UITableViewCell { @IBOutlet weak var catNameLabel: UILabel! /* もちろん、ネコの行儀の悪さは変化します。 - 2つのボタンを設けてこれに対応します。 -- 行儀悪さのランクをアップ -- 行儀悪さのランクをダウン */ @IBAction func actionMoveUp(){ catTableView.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ catTableView.moveCatDown(forCatNumber: catID) } /* そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。 */ var catTableView: catList! /* また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります... */ var catID: Int! }強い参照 (Strong Reference) に気付きましたか?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell let catName = myCats[indexPath.row] cell.catNameLabel.text = catName cell.catTableView = self cell.catID = indexPath.row return cell }これは「naughtyCell」が「catList」を参照している例です。
var catTableView: catList!これは「catList」が「naughtyCell」を参照している例です。
cell.catTableView = selfこれらの2つのコンポーネントは互いを参照しています。:
さて、私たちは「catTableView.moveCatUp(forCatNumber: catID)」と「catTableView.moveCatDown(forCatNumber: catID)」を呼び出す必要があるため、「naughtyCell」をそれぞれの「naughtyCell」に保存する必要があることを思い出してください。解決策は?「プロトコルデリゲート」を使うことです。
デリゲート (Delegate / Protocol) は、2つのプログラミングクラス間で情報を渡すメディアのようなものです。2つの関数を持つデリゲートを作成できます:
protocol catListActionDelegate: AnyObject { func moveCatUp(forCatNumber: Int) func moveCatDown(forCatNumber: Int) }次に、型「catListActionDelegate」を持つ変数を「naughtyCell」に導入し、代わりにそのデリゲート内の関数を呼び出します。
weak var delegate: catListActionDelegate?そして、ユーザーがアクションを実行したときに、デリゲート内の関数を呼び出します。
@IBAction func actionMoveUp(){ delegate?.moveCatUp(forCatNumber: catID) } @IBAction func actionMoveDown(){ delegate?.moveCatDown(forCatNumber: catID) }catList(UITableView)で、デリゲートを実装します:
class fixedCatList: UITableViewController, catListActionDelegateそして、必要とされるデリゲート関数を実装する必要があります。デリゲート内の関数名が「catList」内の既存の関数名と一致するため、ここでは何もする必要はありません。
新しいメモリグラフ
- 投稿日:2020-05-19T01:21:56+09:00
【iOS】iOSアプリ開発入門~ SwiftとUIの接続編1~
前回はiOSアプリ開発をするためのUIを配置するためのAuto Layoutについて投稿しました。
レイアウト編2:https://qiita.com/euJcIKfcqwnzDui/items/31f6b11afb317275f684今回はいよいよSwiftコードとStoryboardを接続していきます。
とはいっても接続はかなり簡単でおそらくすぐに覚えられるかと思います。プログラムと接続するとどうなるの?
そんなんわかるわ!と思う方は読み飛ばしてください。
当たり前ですがソースコードとUIを結び付けなければアプリとして成り立ちません。
アプリの基本的な流れとしては
1.ユーザ操作を受け付ける
2.操作に合わせた処理をする
3.処理結果をユーザにフィードバックする
という流れになります。UI、つまりユーザインタフェースはその名の通りユーザとPCの仲介役です。
ユーザが関わることはすべてUIを通して行われます。
上の例でいくと1,3がUIの仕事です。
そして実際の処理を行うのがプログラムというわけです。
つまりUI->Swift->UI...という流れを実現するために接続します。ボタンのタップ処理
とりあえず接続してみる
それではさっそく接続してみましょう。
前回から使い続けてるプロジェクトを開き、Main.storyboardを開いてください。まずUIとSwiftコードを紐付けるためにはstoryboardとswiftファイル両方を開く必要があります。
Inspectorのすぐ左にある[+]のようなボタンを押してください。
画面が二分割されます。
Inspectorは邪魔なので一度非表示にしましょう。
右上の青くなっているボタンをクリックすると非表示になります。
もう一度クリックすると再度表示されるのでご心配なく。
Swiftファイルを開きます。どちらでも構いませんが私の画面と合わせるために左側の画面の適当な位置をクリックしてください。
その後ファイル一覧からViewController.swiftを選択します。
ViewController.swiftが開かれましたか?
それでは接続します。
まずstoryboardでボタンを選択します。
controlを押しながらボタンをドラッグします。
ボタンとマウスカーソルの間に青い線が出るのでその先端をclass ViewController: UIViewController {
の1行下まで持っていき離してください。
持っていったところあたりから下図のようなポップアップが表示されればOKです。
ポップアップを以下のように変更し、[Connect]ボタンを選択します。
・Connection:Action
・Name:tapButton
以下のように
@IBAction func tapButton(_ sender: Any) {
}
が追加されていれば成功です。
これでボタンのタップ処理が接続されました。
実際に接続されているか確かめてみましょう。tapButtonの{}の中にprint("Hello World")と入力してください。
ViewController.swift@IBAction func tapButton(_ sender: Any) { print("Hello World") }シミュレータで実行し、ボタンをタップしてみてください。
Xcodeの下の方の画面に「Hello World」と表示されました。
またボタンを押せば押した回数分表示されるので試してみてください。
※Xcodeの下にそんな画面がないという方はXcodeの右上にある3つのボタンの真ん中をクリックしてください。
内容の解説
まずボタンをマウスで引っ張っていったときに表示されたポップアップについて。
このポップアップでは接続方法の設定を行っています。
少しプログラミングのオブジェクト指向の用語が出てきますが1つずつ見ていきましょう。
- Connection
- 接続の方法を指定します。後述しますが接続には種類があり、イベントを接続する方法とUI自体を接続する方法があります。今回はボタンに対するイベントを接続したかったので
Action
を選択しました。- Object
- ここはあまり変更することはありませんが接続先のオブジェクトを指定します。
storyboardを確認してください。UIの階層構造の中にView Controller
というUIがあるかと思います。これのことです。
実はこのView ControllerとViewController.swiftはプロジェクト作成時に自動的に紐付けられています。
とりあえずはViewController.swiftの中に処理が書かれるんだなぁと思ってください
(※正確にはViewControllerというクラスから作られたオブジェクトに紐付いています)- Name
- イベントが発生したときに呼ばれるメソッドの名前を指定します。メソッドとは処理そのものというように認識してください。
今回tapButton
と名付けましたが、そのままボタンがタップされたときの処理というような意味で名付けています。- Type
- 1つ目は引数の型を指定します。型とは種類のようなものです。
引数とは@IBAction func tapButton(_ sender: Any)
とありますがsender
のことです。senderの実態は操作されたUI自体、今回でいうとボタン自体です。タップしたときにボタン自体も一緒に渡しますよという感じです。
TypeをAny
で指定したのでsender: Any
senderはAny型ですよとなっています。Anyはボタンに限らずどんな種類になるかわかりませんというようなイメージです。そんなものがあるのかーくらいでいいです。- 2つ目はイベントの詳細を指定します。一口にボタンのイベントといっても「ボタンをタップした」、「ボタンをタップしたけどキャンセルした」など様々なイベントが考えられます。今回指定した[Touch Up Inside]はボタンをタップしたというイベントです
- 3つ目は引数の種類です。
sender
は先述したようにボタン自体です。他にもイベントの情報、例えばどの座標がタップされたというようなものも指定できます。以上が接続方法それぞれの項目の内容です。
今はわからないことが多いかもしれませんが、もう少し勉強を進めて見直すとわかってくるかと思うのでので今はイメージを掴むだけでもいいかと思います。次は接続された後の解説です。
@IBAction func tapButton(_ sender: Any)
の左側を見ると●のようなマークが出ています。
これは「このメソッドは何かのUIと紐付いている」という意味です。先程ボタンと紐付けたため表示されました。
Interface Builderと紐付いているものにはこれが付きます。
また@IBAction
とあります。これは「このメソッドはInterface Builder(IB)からActionとして紐付けられるもの」というような意味です。Interface Builderから紐付けられないメソッドには付きません。
今度はstoryboardを見てみます。
ボタンを選択し右クリック or 二本指でクリックしてください。小さいウィンドウが表示されます。
[Sent Events]のなかに[Touch Up Inside]があり、右側に[View Controller]、[tapButton:]が表示されています。
これはソースコードと紐付いている証で「Touch Up InsideしたときView ControllerのtapButtonが呼び出されますよ」ということです。
View Controllerの右の[✕]ボタンをクリックすると接続が解除されます。
また再度tapButtonまでボタンをドラッグすると紐付けることができます。
備考として[Sent Events]のなかに[Touch Up Inside]以外にも色々あります。これが先述したイベントの詳細です。
ViewController.swiftに戻りましょう。
接続後print("Hello World")
というコードを書きました。
これはデバッグコンソールに「Hello World」と表示するという処理です。
デバッグコンソールは先程「Hello World」と表示された場所です。
print()はデバッグコンソールに文字列を表示するための関数で開発の際よく使います。
(関数とはメソッドと似たようなもので処理の塊というイメージです)ViewController.swift@IBAction func tapButton(_ sender: Any) { print("Hello World") }ということで
ボタンをタップ → tapButtonが呼ばれる → print()が呼ばれる
という流れで処理が実行されたというわけです。最後に
今回はボタンタップ処理のの仕方について説明しました。
プログラミングに初めて触るという方にとっては聞いたことがない単語がいくつか出てきたかと思いますが、実際に行った作業としては単純なものだったのではないでしょうか?
また簡単な処理ではありますが少しずつアプリとして動くようになってきているかと思います。
この調子で着実に進めましょう。プログラミングの基本については近々Swiftベースでまとめます。
次回はボタンタップからのUI更新の方法を紹介しようと思います。
SwiftとUIの接続編2:https://qiita.com/euJcIKfcqwnzDui/items/93f010b989a4d333f0b9本連載ではプログラミング未経験からiOSアプリ開発が行えるようになることを目的としています。
今までの投稿をまとめていますのでこちらもご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684