20200524のSwiftに関する記事は14件です。

【iOS】XCUIElementTypeQueryProviderの定義済みクエリとUI要素の対応

XCUITestでUITestを作成する場合、UI要素の検索でXCUIElementTypeQueryProviderの定義済みクエリを
使うこともあると思います。
定義済みクエリを使用する時、クエリと実際のUI要素との対応が一部曖昧な部分があったので、
よく使いそうなUI要素との対応を備忘録的に記録しておこうと思います。

UILabel

    func test_UILabelを取得() {
        let label = app.staticTexts["UILabel"] // accessibility identifierを指定
        XCTAssertTrue(label.exists) // 指定したaccessibility identifierのUI要素が存在するか確認
    }

UIImageView

    func test_UIImageViewを取得() {
        let imageView = app.images["UIImageView"]
        XCTAssertTrue(imageView.exists)
    }

UITextField(secureTextEntryではない)

    func test_UITextFieldを取得() {
        let textField = app.textFields["UITextField"]
        XCTAssertTrue(textField.exists)
    }

UITextField(secureTextEntryである)

    func test_secureTextEntryのUITextFieldを取得() {
        let secureTextField = app.secureTextFields["UITextField_Secure"]
        XCTAssertTrue(secureTextField.exists)
    }

UIButton

    func test_UIButtonを取得() {
        let button = app.buttons["UIButton"]
        XCTAssertTrue(button.exists)
    }

UITextView

    func test_UITextViewを取得() {
        let textView = app.textViews["UITextView"]
        XCTAssertTrue(textView.exists)
    }

UISegmentedControl

    func test_UISegmentedControlを取得() {
        let segmentedControl = app.segmentedControls["SegmentedControl"]
        XCTAssertTrue(segmentedControl.exists)
    }

UISlider

    func test_UISliderを取得() {
        let slider = app.sliders["UISlider"]
        XCTAssertTrue(slider.exists)
    }

UISwitch

    func test_UISwitchを取得() {
        let uiSwitch = app.switches["UISwitch"]
        XCTAssertTrue(uiSwitch.exists)
    }

UIView

    func test_UIViewを取得() {
        let view = app.otherElements["UIView"]
        XCTAssertTrue(view.exists)
    }

終わりに

上記のUI以外のクエリは以下のリンクをご参照ください。
定義済みクエリを全て確認できます。
クエリはiOS, macOSの区別なく定義されているので一部iOSでは使えないものもあります。(touchBarなど)
https://developer.apple.com/documentation/xctest/xcuielementtypequeryprovider

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

R.Swiftでビルドエラー(Pods/R.swift/rswift: No such file or directory)が発生

事象

これです。よく見るやつですね。

Pods/R.swift/rswift: No such file or directory
Command PhaseScriptExecution failed with a nonzero exit code

Google先生に聞けばCleanすれば直るとか諸々削除してpod installすれば直るとかBuildConfigの設定が間違っているとか、色々な解消法が出てくるかと思います。

今回、数分前までBuildできていたものが、いきなりBuildできなくなり
一般的な解消方法を全部やってもダメで、数時間潰しました。

■一般的な方法 (引用元 https://qiita.com/masa-321/items/19f66557dcff65553f18)

・Clean(Command + Shift + K)
・DerivedDataファイル以下全ての中間ファイル(キャッシュ?)を削除
参考:[Xcode][小ネタ] DerivedDataの削除についての備忘録
・Podfileを編集して、Podの再インストール。
・最近アップデートしたライブラリがあれば、ダウングレードして試してみる。
・Cocoa Podsのアップデート
・Carthageのアップデート
・キーチェーンのログインをロック&解除を1度繰り返す
・古いMacbookの証明書をコピー
参考:iOSアプリ開発で実機による開発を複数台(メイン機ではない2台目以降)のMacで行いたい場合
・Build Phases>Run Script>Shellにて、タイピングミスがないか(余計なスペースが入っているケースがある)・・・①
・Xcodeの再起動
・Macbook本体の再起動

原因

アンチウイルスソフトが生成されたPods/R.swift/rswiftを生成される度に消してました。
F**k

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

R.swiftでビルドエラー(Pods/R.swift/rswift: No such file or directory)が発生

事象

これです。よく見るやつですね。

Pods/R.swift/rswift: No such file or directory
Command PhaseScriptExecution failed with a nonzero exit code

Google先生に聞けばCleanすれば直るとか諸々削除してpod installすれば直るとかBuildConfigの設定が間違っているとか、色々な解消法が出てくるかと思います。

今回、数分前までBuildできていたものが、いきなりBuildできなくなり
一般的な解消方法を全部やってもダメで、数時間潰しました。

■一般的な方法 (引用元 https://qiita.com/masa-321/items/19f66557dcff65553f18)

・Clean(Command + Shift + K)
・DerivedDataファイル以下全ての中間ファイル(キャッシュ?)を削除
参考:[Xcode][小ネタ] DerivedDataの削除についての備忘録
・Podfileを編集して、Podの再インストール。
・最近アップデートしたライブラリがあれば、ダウングレードして試してみる。
・Cocoa Podsのアップデート
・Carthageのアップデート
・キーチェーンのログインをロック&解除を1度繰り返す
・古いMacbookの証明書をコピー
参考:iOSアプリ開発で実機による開発を複数台(メイン機ではない2台目以降)のMacで行いたい場合
・Build Phases>Run Script>Shellにて、タイピングミスがないか(余計なスペースが入っているケースがある)・・・①
・Xcodeの再起動
・Macbook本体の再起動

原因

アンチウイルスソフトが生成されたPods/R.swift/rswiftを生成される度に消してました。
F**k

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

iOSのBageを標準とは違う位置に無理やりつける方法

ごくまれに、TabBarのバッチの位置をカスタムしたいとお願いされることがあります。
無理やりバッチをつけるのはちょっとめんどくさいですが、上手くできたので、メモっておきます。

class CustomTabViewController: UITabBarController {

    private var badges: [UIView] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        addBage(position: 3, text: "11", frame: CGRect(x: 30, y: 5, width: 18, height: 18))
    }

extension MainTabViewController {

    // バッチをつける
    private func addBage(position: Int, text:String, frame:CGRect) {
        guard tabBar.subviews.count > position else {
            return
        }
        let tabBarButton = tabBar.subviews[position]
        for subView in tabBarButton.subviews {
            guard let icon = subView as? UIImageView else { continue }
            let badge = createBage(text: text, frame: frame)
            icon.addSubview(badge)
            badges.append(badge)
        }
    }

    // バッチを全て剥がす
    private func removeAllBage() {
        badges.forEach { bage in
            bage.removeFromSuperview()
        }
    }

    // バッジを生成
    private func createBage(text: String, frame: CGRect) -> UIView {
        let bageView = UILabel(frame: frame)
        bageView.text = text
        bageView.textAlignment = .center
        bageView.font = .systemFont(ofSize: 13)
        bageView.adjustsFontSizeToFitWidth = true
        bageView.minimumScaleFactor = 0.5
        bageView.textColor = .white
        bageView.layer.cornerRadius = 9
        bageView.layer.masksToBounds = true
        bageView.backgroundColor = .red
        return bageView
    }
}
}

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

【swift 5】 dateFormat の多国籍対応

アプリを多国籍対応する時に、年月日の表示を国別に変える必要があります。
日本人が使う時は、2020年5月24日
アメリカ人が使う時は、May 24, 2020
と表示させるには以下のようにします。

let formatter = DateFormatter()
formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "ydMMM", options: 0, locale: .autoupdatingCurrent)

locale: .autoupdatingCurrent とすることでユーザーが選んだ「使用する言語の優先順序」第一位の言語が選択されます。

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

【swift 5】 dateFormat の多言語対応

アプリを多言語対応する時に、年月日の表示を国別に変える必要があります。
日本人が使う時は、2020年5月24日
アメリカ人が使う時は、May 24, 2020
と表示させるには以下のようにします。

let formatter = DateFormatter()
formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "ydMMM", options: 0, locale: .autoupdatingCurrent)

locale: .autoupdatingCurrent とすることでユーザーが選んだ「使用する言語の優先順序」第一位の言語が選択されます。

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

[Swift / Xcode / Realm]Realm を利用した ToDoアプリの実装(ToDo の追加・編集・更新・削除)

この記事の内容

モバイル向けデータベース管理システム Realm を利用した、ToDo アプリの開発
スクリーンショット 2020-05-24 13.05.26.png

参考にした記事

Realmを使ってTODOアプリを作ってみよう!/ Swift(Realmの使い方、初級編)

開発したアプリ

前掲の記事を参考にして、ToDo を追加しテーブルビューで表示することに加えて下記機能を実装した。
GitHub にもコードを UP しています。
GitHub - tanakadaichi1989

Remove All ボタン をクリックすると、ToDo を全て削除
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.27.pngSimulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.33.png

・ToDo を表示しているテーブルビューのセルを左にスライドすると、その ToDo のみを削除
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.11.png

・ToDo を表示しているテーブルビューのセルをクリックすると、ToDo を編集するためのアラートコントローラを表示
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.19.png

・アラートコントローラの OKボタン をクリックすると、ToDo を更新
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.23.pngSimulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.27.png

前提条件

① CocoaPods Realm の導入、テキストボックスに入力した文字を保存する処理を実装する方法は前掲の記事を参照してください。

② 今回のアプリは、Realm の利用方法を確認するために開発しました。その為、 Apple が策定した Human Interface Guidelines は考慮していません。

開発環境

項目
PC MacBook Air 2017
IDE Xcode Ver. 11.4.1
言語 Swift 5.2.2
データベース Realm
コード管理 GitHub

Realm とは

モバイル向けデータベース管理システム

Main.storyboard

項目 属性 備考
ViewCotroller UIViewController ViewCotroller.swift と紐付け
Navigation Bar UINavigationBar Title を "TestApp" と設定
ToDoTextField UITextField このテキストフィールドに入力した文字を ToDo として保存
Add Button UIButton ボタンの色:青 角は丸く
Remove All Button UIButton ボタンの色:赤 角は丸く
ToDoTableView UITableView 保存されている ToDo を表示
testCell UITableViewCell Identifier を "testCell" と設定

コード

ToDoModel.swift
import Foundation
import RealmSwift

class TodoModel: Object{
    @objc dynamic var todo: String? = nil
}
ViewCotroller.swift
import UIKit
import RealmSwift

class ViewController: UIViewController,UITextFieldDelegate,UITableViewDelegate {

    @IBOutlet weak var todoTextFiled: UITextField!
    @IBOutlet weak var todoTableView: UITableView!
    @IBOutlet weak var addButton: UIButton!
    @IBOutlet weak var removeAllButton: UIButton!
    var itemList: Results<TodoModel>!

    // Add ボタンをクリックした際に実行する処理
    @IBAction func tapAddButton(_ sender: Any) {
        let instancedTodoModel:TodoModel = TodoModel()
        instancedTodoModel.todo = self.todoTextFiled.text

        let realmInstance = try! Realm()
        try! realmInstance.write{
            realmInstance.add(instancedTodoModel)
        }
        self.todoTableView.reloadData()
    }

    // Remove All ボタンをクリックした際に実行する処理
    @IBAction func tapRemoveAllButton(_ sender: Any) {
        let realmInstance = try! Realm()
        try! realmInstance.write{
            realmInstance.deleteAll()
        }
        self.todoTableView.reloadData()
    }


    override func viewDidLoad() {
        super.viewDidLoad()
        // UITableViewDataSource を self に設定
        self.todoTableView.dataSource = self
        // UITableViewDelegate を self に設定
        self.todoTableView.delegate = self

        // ボタンの角を丸くする設定
        addButton.layer.cornerRadius = 5
        removeAllButton.layer.cornerRadius = 5

        let realmInstance = try! Realm()
        self.itemList = realmInstance.objects(TodoModel.self)
        self.todoTableView.reloadData()
    }
}

extension ViewController: UITableViewDataSource{

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.itemList.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let testCell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "testCell")!
        let item: TodoModel = self.itemList[(indexPath as NSIndexPath).row]
        testCell.textLabel?.text = item.todo
        return testCell
    }

    // テーブルビューの編集を許可
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // テーブルビューのセルとデータを削除
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == UITableViewCell.EditingStyle.delete {
            // データを削除
            let realmInstance = try! Realm()
            try! realmInstance.write {
                realmInstance.delete(itemList[indexPath.row])
            }
            // セルを削除
            tableView.deleteRows(at: [indexPath as IndexPath], with: UITableView.RowAnimation.automatic)
        }
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("セル\(indexPath)が選択されました")
        showAlertController(indexPath)
    }

    // テーブルビューのセルをクリックしたら、アラートコントローラを表示する処理
    func showAlertController(_ indexPath: IndexPath){
        let alertController: UIAlertController = UIAlertController(title: "\(String(indexPath.row))番目の ToDo を編集", message: itemList[indexPath.row].todo, preferredStyle: .alert)
        // アラートコントローラにテキストフィールドを表示 テキストフィールドには入力された情報を表示させておく処理
        alertController.addTextField(configurationHandler: {(textField: UITextField!) in
            textField.text = self.itemList[indexPath.row].todo})
        // アラートコントローラに"OK"ボタンを表示 "OK"ボタンをクリックした際に、テキストフィールドに入力した文字で更新する処理を実装
        alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: {
            (action) -> Void in self.updateAlertControllerText(alertController,indexPath)
        }))
        // アラートコントローラに"Cancel"ボタンを表示
        alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        self.present(alertController, animated: true, completion: nil)
    }

    // "OK"ボタンをクリックした際に、テキストフィールドに入力した文字で更新
    func updateAlertControllerText(_ alertcontroller:UIAlertController, _ indexPath: IndexPath) {
        // guard を利用して、nil チェック
        guard let textFields = alertcontroller.textFields else {return}
        guard let text = textFields[0].text else {return}

        // UIAlertController に入力された文字をコンソールに出力
        print(text)

        // Realm に保存したデータを UIAlertController に入力されたデータで更新
        let realmInstance = try! Realm()
        try! realmInstance.write{
            itemList[indexPath.row].todo = text
        }
        self.todoTableView.reloadData()
    }
}

実装のポイント

・テーブルビューのセルを削除するだけでは、それに紐づく Realm に保存したデータは削除されない。そのため、セルとデータをそれぞれ削除する処理を実装する必要がある。

感想

・CocoaPods Realm の導入に少しつまづいた
・アラートコントロールを利用した編集・更新処理のコードが煩雑になったので、リファクタリングしたい
・データベースと連携した iOS アプリを開発できるようになり、開発の幅が広まったと思う
Human Interface Guidelines に沿うように UI を改善したい

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

Realmを利用したiOSアプリの実装(データの追加・編集・更新・削除)

この記事の内容

モバイル向けデータベース管理システム Realm を利用した、iOSアプリの開発
スクリーンショット 2020-05-24 13.05.26.png

参考にした記事

Realmを使ってTODOアプリを作ってみよう!/ Swift(Realmの使い方、初級編)

開発したアプリ

前掲の記事を参考にして、ToDo を追加しテーブルビューで表示することに加えて下記機能を実装した。

Remove All ボタン をクリックすると、ToDo を全て削除
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.27.pngSimulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.33.png

・ToDo を表示しているテーブルビューのセルを左にスライドすると、その ToDo のみを削除
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.11.png

・ToDo を表示しているテーブルビューのセルをクリックすると、ToDo を編集するためのアラートコントローラを表示
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.19.png

・アラートコントローラの OKボタン をクリックすると、ToDo を更新
Simulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.23.pngSimulator Screen Shot - iPhone SE (2nd generation) - 2020-05-24 at 12.47.27.png

前提条件

① CocoaPods Realm の導入、テキストボックスに入力した文字を保存する処理を実装する方法は前掲の記事を参照してください。

② 今回のアプリは、Realm の利用方法を確認するために開発しました。その為、 Apple が策定した Human Interface Guidelines は考慮していません。

開発環境

項目
PC MacBook Air 2017
IDE Xcode Ver. 11.4.1
言語 Swift 5.2.2
データベース Realm
コード管理 GitHub

Realm とは

モバイル向けデータベース管理システム

Main.storyboard

項目 属性 備考
ViewCotroller UIViewController ViewCotroller.swift と紐付け
Navigation Bar UINavigationBar Title を "TestApp" と設定
ToDoTextField UITextField このテキストフィールドに入力した文字を ToDo として保存
Add Button UIButton ボタンの色:青 角は丸く
Remove All Button UIButton ボタンの色:赤 角は丸く
ToDoTableView UITableView 保存されている ToDo を表示
testCell UITableViewCell Identifier を "testCell" と設定

コード

ToDoModel.swift
import Foundation
import RealmSwift

class TodoModel: Object{
    @objc dynamic var todo: String? = nil
}
ViewCotroller.swift
import UIKit
import RealmSwift

class ViewController: UIViewController,UITextFieldDelegate,UITableViewDelegate {

    @IBOutlet weak var todoTextFiled: UITextField!
    @IBOutlet weak var todoTableView: UITableView!
    @IBOutlet weak var addButton: UIButton!
    @IBOutlet weak var removeAllButton: UIButton!
    var itemList: Results<TodoModel>!

    // Add ボタンをクリックした際に実行する処理
    @IBAction func tapAddButton(_ sender: Any) {
        let instancedTodoModel:TodoModel = TodoModel()
        instancedTodoModel.todo = self.todoTextFiled.text

        let realmInstance = try! Realm()
        try! realmInstance.write{
            realmInstance.add(instancedTodoModel)
        }
        self.todoTableView.reloadData()
    }

    // Remove All ボタンをクリックした際に実行する処理
    @IBAction func tapRemoveAllButton(_ sender: Any) {
        let realmInstance = try! Realm()
        try! realmInstance.write{
            realmInstance.deleteAll()
        }
        self.todoTableView.reloadData()
    }


    override func viewDidLoad() {
        super.viewDidLoad()
        // UITableViewDataSource を self に設定
        self.todoTableView.dataSource = self
        // UITableViewDelegate を self に設定
        self.todoTableView.delegate = self

        // ボタンの角を丸くする設定
        addButton.layer.cornerRadius = 5
        removeAllButton.layer.cornerRadius = 5

        let realmInstance = try! Realm()
        self.itemList = realmInstance.objects(TodoModel.self)
        self.todoTableView.reloadData()
    }
}

extension ViewController: UITableViewDataSource{

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.itemList.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let testCell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "testCell")!
        let item: TodoModel = self.itemList[(indexPath as NSIndexPath).row]
        testCell.textLabel?.text = item.todo
        return testCell
    }

    // テーブルビューの編集を許可
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // テーブルビューのセルとデータを削除
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == UITableViewCell.EditingStyle.delete {
            // データを削除
            let realmInstance = try! Realm()
            try! realmInstance.write {
                realmInstance.delete(itemList[indexPath.row])
            }
            // セルを削除
            tableView.deleteRows(at: [indexPath as IndexPath], with: UITableView.RowAnimation.automatic)
        }
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("セル\(indexPath)が選択されました")
        showAlertController(indexPath)
    }

    // テーブルビューのセルをクリックしたら、アラートコントローラを表示する処理
    func showAlertController(_ indexPath: IndexPath){
        let alertController: UIAlertController = UIAlertController(title: "\(String(indexPath.row))番目の ToDo を編集", message: itemList[indexPath.row].todo, preferredStyle: .alert)
        // アラートコントローラにテキストフィールドを表示 テキストフィールドには入力された情報を表示させておく処理
        alertController.addTextField(configurationHandler: {(textField: UITextField!) in
            textField.text = self.itemList[indexPath.row].todo})
        // アラートコントローラに"OK"ボタンを表示 "OK"ボタンをクリックした際に、テキストフィールドに入力した文字で更新する処理を実装
        alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: {
            (action) -> Void in self.updateAlertControllerText(alertController,indexPath)
        }))
        // アラートコントローラに"Cancel"ボタンを表示
        alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        self.present(alertController, animated: true, completion: nil)
    }

    // "OK"ボタンをクリックした際に、テキストフィールドに入力した文字で更新
    func updateAlertControllerText(_ alertcontroller:UIAlertController, _ indexPath: IndexPath) {
        // guard を利用して、nil チェック
        guard let textFields = alertcontroller.textFields else {return}
        guard let text = textFields[0].text else {return}

        // UIAlertController に入力された文字をコンソールに出力
        print(text)

        // Realm に保存したデータを UIAlertController に入力されたデータで更新
        let realmInstance = try! Realm()
        try! realmInstance.write{
            itemList[indexPath.row].todo = text
        }
        self.todoTableView.reloadData()
    }
}

実装のポイント

・テーブルビューのセルを削除するだけでは、それに紐づく Realm に保存したデータは削除されない。そのため、セルとデータをそれぞれ削除する処理を実装する必要がある。

感想

・CocoaPods Realm の導入に少しつまづいた
・アラートコントロールを利用した編集・更新処理のコードが煩雑になったので、リファクタリングしたい
・データベースと連携した iOS アプリを開発できるようになり、開発の幅が広まったと思う
Human Interface Guidelines に沿うように UI を改善したい

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

【Swift5】自作クラス(と配列)をUserDefaultsに簡単に保存する

はじめに

初めまして。Lukeです。

この記事では自作クラスをUserDefaultsに簡単に保存する関数を紹介します。

ちょっとしたデータの保存はUserDefaultsを使うのが一般的ですが、保存できる型が限られています。
対応していない場合はDataに変換して保存する必要があり、少し面倒です。
そこで今回は関数(と言っても大したことはないですが)を作成しました。

保存したいクラス

class Friend: NSObject, NSCoding {
  var name:String?
  var age:Int?

  init(_ name: String?, _age: Int?) {
    self.name = name
    self.age= age
  }

  func encode(with coder: NSCoder) {
      coder.encode(name, forKey: "name")
      coder.encode(age, forKey: "age")
  }

  required init?(coder: NSCoder) {
      name = coder.decodeObject(forKey: "name") as? String
      age = coder.decodeObject(forKey: "age") as? Int
  }
}

ポイントは1行目に: NSObject, NSCodingと加えることと、エンコード・デコードの部分を用意することです。

保存、取得用の関数

通常

extension UserDefaults {
  func getFriend(_ key:String) -> Friend? {
      if let storedData = UserDefaults.standard.object(forKey: key) as? Data {
          if let unarchivedObject = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(storedData) as? Friend {
              return unarchivedObject
          }
      }
      return nil
  }

  func setFriend(_ friend:[Friend],_ key:String) {
      let data = try! NSKeyedArchiver.archivedData(withRootObject: friend, requiringSecureCoding: false)
      UserDefaults.standard.set(data, forKey: key)
  }
}

配列の場合

extension UserDefaults {
  func getFriends(_ key:String) -> [Friend]? {
      if let storedData = self.object(forKey: key) as? Data {
          if let unarchivedObject = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(storedData) as? [Friend] {
              return unarchivedObject
          }
      }
      return nil
  }

  func setFriends(_ friends:[Friend],_ key:String) {
      let data = try! NSKeyedArchiver.archivedData(withRootObject: friends, requiringSecureCoding: false)
      self.set(data, forKey: key)
  }
}

使用方法

この関数はUserDefaultsの拡張なので、このように使います。

通常

let boy = Friend.init("Tom",25)
//保存
UserDefaults.standard.setFriend(boy,"friend")
//取得
let friend = UserDefaults.standard.getFriend("friend")

配列の場合

let boys = [Friend.init("Tom",25),Friend.init("Bob",18)]
//保存
UserDefaults.standard.setFriends(boys,"friends")
//取得
let friends = UserDefaults.standard.getFriends("friends")

おわりに

通常のUserDefaultsのように1行で使えるので、コードが見やすくなると思います。

間違っている部分があったら教えていただけると助かります?‍♂️

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

【プログラミング初心者】Swift基礎~変数・型~

プログラミング初心者、初級者に向けてSwiftをベースとした変数宣言、型の種類について紹介していきます。

変数

プログラミングでは処理をする際様々なデータを扱います。
このデータはコンピュータの記憶領域(メモリ)に保存されます。
プログラミングではこのメモリに変数名前を付け、変数経由でメモリの読み書きをします。

変数を扱う前には「今からこんな名前の変数を使います」とコンピュータに教えてあげる必要になります。
これを変数を宣言するとか定義するとか言ったりします。
以下の例はxという名前の変数をInt型で宣言しています。(※型については後述)

var integer: Int

宣言した後はintegerという名前でメモリにアクセスすることができます。
integerという名前の箱をメモリに準備し、その中のデータを参照したり更新するというようなイメージです。

変数・定数

先述した説明ではデータの扱いはまとめて変数といってますが実際には変数と定数があります。
それほど違いはなくイメージのまま変数は読み書き可能で、定数の場合書き込みは初期化の時のみでその後はデータの更新はできません。
宣言の仕方が変数と定数では少し異なり、変数の場合はvar、定数の場合はletをそれぞれ宣言時に付けます。

var variable: Int = 0
variable = 10 // 変数は書き換え可能

let constant: Int = 0
constant = 10 // 定数は読み込みのみなのでエラー

書き換えの必要がないものにはletを使ってあげましょう。

型について

変数を扱う際は型というものを意識します。
型とは扱う変数がどんな種類でどんな性質を持っているのかなど、そのデータの特性を表すものです。
代表的なものとしては文字列を扱うString型、整数を扱うInt型などが挙げられます。

変数は指定された型しか扱えません。
例えばInt型で宣言した変数に文字列を格納しようとしてもプログラムは理解できずエラーとなります。

var integer: Int = 0
integer = "文字列を代入" // 型が異なるためエラー

(補足ですが文字列は"~"で表されます)

このように明確に型を付けてあげることを型アノテーションといいます。

もし型というものがなければ、例えば金額の計算をしていたのにいつの間にか「ああああ111」という結果になってしまうということもありえます。
そうなるとアプリが実装者の意図した挙動をしない可能性が出てきます。それを未然に防ぐためにSwift自体がエラーを出してくれます。

余談ですがSwiftなどはコンパイラ言語と呼ばれ型宣言を必要としています。一方でスクリプト言語と呼ばれるJavaScriptやPHPなどでは型がありません。
そのためスクリプト言語ではいつの間にか変数が文字列に変わっていたということは発生したりします。これは実装者がそうならないように気をつけて実装していく必要があります。

メモリアドレス

少し変数宣言について内部処理の話をします。少し難しいかもしれないので、理解できなければそんな感じなのかーくらいの認識を持つ程度で大丈夫です。
そこまで気にしなくてもSwiftはとりあえずは書けます。

実際、変数にはアドレスと呼ばれるメモリの場所、住所が記録されています。
メモリはコンピュータから1bitに対し1アドレスが割り当てられています。(1byte毎かもしれません。。)
アドレスは整数で定義されており16進数で表されることが多いです。
そしてメモリ1つ1つに対して0 or 1のデータが格納されています。
メモリ1.png

変数は扱うデータを表すメモリ領域の先頭のアドレスを持ちます。
先頭アドレスがわかればどこにデータの内容が確保されているかわかります。
では先頭はわかりましたがこの変数はどのメモリまでをデータとして扱えばいいのでしょうか?
それを表すのが型です。
型にはそれぞれ必要とする長さが決まっています。
(全部がそうではありませんがそういったイメージでいいです)
環境によって変わったりしますがIntなら64bitを確保します。
メモリ2.png

このようにしてどこからどこまでを1つのデータとして扱うかを決めています。
結局扱う際はこのデータのまとまりを1データとして扱うためIntとして大きな箱がメモリに準備されると思ってもらえれば問題ありません。

型推論

さて、ここまで変数には型が必要だと説明してきましたが、実はSwiftでは実装者が型アノテーションで明確に型を宣言しなくても良い場合があります。むしろそのパターンの方が多かったりします。
この機能を型推論といい、読んで字のごとく型を推測してくれます。

例えばIntの場合以下のように定義することができます。

let integer = 0

このように宣言した場合、Swiftの方で勝手にこれは整数を扱っているからInt型だなというように推測してくれ、その後はInt型として扱うことができます。

型推論とはいえSwift側では型を明確にできるためIntと推論されたものに対しては文字列は代入できません。

var integer = 0
integer = "文字列を代入する" // Swift側でエラーとして扱ってくれる

このようにSwift側でエラーを表示してくれるため型の違いによる意図しない挙動は起こらないようになっています。

一方で型アノテーションをしてあげないといけない型もあります。
例えば使うことは少ないですがInt32型などです。Int32型はInt型と違い32bit分の整数しか扱わない型です。
同じ整数型ですがvar integer = 0と宣言するとSwiftはIntと認識するため
明示的にvar integer: Int32 = 0としてあげる必要があります。

特に明確に型を区分する場合は以外は型推論を使ってあげればいいかと思います。

基本型一覧

本項ではSwiftの基本となる型を紹介します。

整数

整数データを扱う型です。
基本的にはInt型を使います。
Int型は型アノテーションを必要としません。

整数型には符号ありと符号なしがあります。符号なしは負の数「-」は扱わないという意味です。
それぞれの型で扱える値の範囲が違います

  • 符号あり
    • Int,Int8,Int16,Int32,Int64
  • 符号なし
    • UInt,UInt8,UInt16,UInt32,UInt64
let integer: Int = 10        // 32bit環境ではInt32、64bit環境ではInt64と同じ
let integer8: Int8 = 10      // -128〜127
let integer16: Int16 = 10    // -32768〜32767
let integer32: Int32 = 10    // -2147483648〜2147483647
let integer64: Int64 = 10    // -9223372036854775808〜9223372036854775807
let uInteger: UInt = 10      // 32bit環境ではUInt32、64bit環境ではUInt64と同じ
let uInteger8: UInt8 = 10    // 0〜255
let uInteger16: UInt16 = 10  // 0〜65535
let uInteger32: UInt32 = 10  // 0〜4294967295
let uInteger64: UInt64 = 10  // 0〜18446744073709551615

少数

主にDouble型とFloat型があります。
それぞれ内部での数値の持ち方が異なり、Doubleの方が正確です。
基本的にはDoubleを使用します。
型アノテーションなしの場合はDouble型になります。

let double: Double = 0.1    // 64bit
let float: Float = 0.1      // 32bit
let float32: Float32 = 0.1  // 32bit
let float64: Float64 = 0.1  // 64bit
let cgFloat: CGFloat = 0.1  // 32bit環境ではFloat32、64bit環境ではFloat64と同じ。UI周りでよく使われている

文字列

String型のみと思っていいです。
型アノテーションなしで使用できます。

let string: String = "文字列"

論理型

Bool型。真偽値を扱います。
値はtrue/falseのみです。
型アノテーションなしで使用できます。

主に条件判定の際に使います。

var boolean: Bool = true
var equalBoolean = 0 == 0

配列

「配列」とは複数の値を格納するための型です。
画像を見たほうが想像が付きやすいと思います。
配列.png

少し注意が必要なのは配列の最初が0番目から始まることです。
数学の数列などでは1番目から扱うことが多いですがそこは異なっています。

配列はArray型ですが、宣言の仕方が少し特殊です。

配列には変数を格納していくため、その変数自体の型情報も一緒に定義してあげる必要があります。
宣言した型以外のものを入れるとSwiftからエラーが表示されます。
宣言の構文はvar 名前: [型] = 初期値です。

var integerArray: [Int] = [0, 1, 5, 19]                 // Intを格納する配列
var stringArray: [String] = ["りんご", "みかん", "バナナ"]  // Stringを格納する配列

配列名[n]で格納した値にアクセスすることができます。

// 参照
print(integerArray[0]) // 0
print(stringArray[2]) // バナナ

// 更新
stringArray[0] = "パイナップル"
print(stringArray[0]) // パイナップル

また初期値は空でもよく、さらに変数として宣言している場合後から追加、削除できます。
追加は配列名.append(追加する要素)
削除は配列名.remove(at: 削除する要素の配列番号)
appendは配列の一番後ろに要素を追加します。他にも間に挿入するなどもあります。

// 配列の初期値を空で宣言
var stringArray: [String] = []
// 追加処理
stringArray.append("りんご")
stringArray.append("みかん")
print(stringArray) // ["りんご", "みかん"]

// 削除処理
stringArray.remove(at: 0)
print(stringArray) // ["みかん"]

補足として配列を定数として宣言した場合、更新はできません。
appendremoveも同様に使えません。

let constantArray: [String] = ["りんご", "みかん"]

// 更新できない
constantArray[0] = "パイナップル" //エラー
constantArray.append("メロン") // エラー
constantArray.remove(at: 0) // エラー

// 参照はできる
print(constantArray) // ["りんご", "みかん"]
print(constantArray[0]) // "りんご"

また、配列は初期値が空でない場合は型アノテーションなしで宣言することができます。

var integerArray = [0] // Int型の配列として定義される
var stringArray = ["りんご"] // String型の配列として定義される
var unknownArray = [] // 型が推測できないためエラー

基本的な情報としてはこれくらいですが、Swiftの配列操作は色々な機能があるので別途まとめます。

辞書

配列と同様に複数の値を格納するための型です。配列を拡張したようなものです。
配列は格納した要素を配列番号(インデックス)で管理しますが、辞書ではキーで管理します。
キーとは検索ワードのようなものをイメージしてもらえるとわかりやすいかと思います。
辞書はkey-valueの形でキーと値をワンセットとして保持します。
辞書.png

辞書はDictionary型ですが配列と同様に値の型、さらにキーの型を一緒に宣言してあげる必要があります。
辞書も当然宣言した型以外のものを入れるとSwiftからエラーが表示されます。
宣言の仕方はvar 名前: [キーの型: 値の型] = 初期値となります。

// キーがString, 値がString
var stringDictionary: [String: String] = ["apple": "りんご", "orange": "オレンジ", "banana": "バナナ"]

// キーがString, 値がInt
var intDictionary: [String: Int] = ["apple": 1, "orange": 1, "banana": 5]

// キーがInt, 値がString
var keyIntDictionary: [Int: String] = [1: "りんご", 3: "オレンジ", 5: "バナナ"]

初期化の際注意が必要なのはキーが重複しないようにしてください。
同じキーを定義してもSwiftからエラーが返ってきませんが、初期化されるタイミングでアプリがクラッシュするようです。
値は同じ値があっても問題ありません。

値へのアクセスは辞書名[キー名]です。

// 参照
print(stringDictionary["apple"]) // りんご
print(intDictionary["banana"]) // 5
print(keyIntDictionary[3]) // オレンジ

// 更新
stringArray["apple"] = "アップル"
print(stringArray["apple"]) // アップル

また当然初期値は空でもよく、変数で宣言した場合は追加/削除することができます。
追加は更新と同じ方法で辞書名[キー名] = 値です。格納先のキーが存在しない場合は新しい要素として追加されます。
削除は辞書名.removeValue(forKey: キー名)

// 配列の初期値を空で宣言
var dictionary: [String: String] = [:]
// 追加処理
dictionary["apple"] = "りんご"
dictionary["orange"] = "オレンジ"
print(dictionary) // ["orange": "オレンジ", "apple": "りんご"]

// 削除処理
dictionary.removeValue(forKey: "apple")
print(dictionary) // ["orange": "オレンジ"]

※意識することは少ないかもしれませんが配列と違い、辞書型では登録した順番は担保されずどのような順番で保持されているかはわかりません。

型宣言についても配列と同様、空以外の初期値を与えてあげると型推論することができます。

このように辞書型は配列と似たような性質であることがわかります。
配列はインデックスという順番で管理し、辞書はキーという実装者が見て意味がわかりやすいもので管理すると覚えておいてください。

余談ですが辞書型は別の言語では連想配列と呼ばれたりもします。

最後に

変数、型、基本型の種類にについては以上です。
これくらいあればある程度表現できるではないでしょうか?

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

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

Swift プログラムのデバッグテクニック - ブレイクポイント、ランタイム変数検査

この記事では、一般的なバグ修正方法をいくつかご紹介します。

一般的なデバッグ手法

print, print, print...

Printステートメントは常に役に立ちます。それにより、プログラムのランタイム中に変数を出力できます。

func dailyRoutine(){
    print(myCats)
    myCats.forEach { (cat) in
        print("猫の名前は\(cat.name)です。")
        cat.feed()
        cat.hug()
    }
}

ブレークポイント

行番号をクリックしてブレークポイントを Xcode に設定できます。プログラムは、シミュレーターか接続したiOSデバイス上で実行しているとき、ブレークポイントに到達すると停止します。ブレークポイントには以下のようないくつもの種類があります。

通常のブレークポイント

通常のブレークポイントを作成するには、Xcode のコード行の行番号をクリックします:

ezgif-2-df0a16ddb28f.gif

プログラムが停止すると以下のようになります:

Screen Shot 2020-05-23 at 16.56.08.png

条件付きブレークポイント

条件付きブレークポイントは、ある特定の条件を満たした場合にのみコードを停止させます。例えば、ネコの名前が "ネコノヒー" : の場合にのみ、プログラムの実行を停止させたいならば

ezgif-2-ded322b7d123.gif

Screen Shot 2020-05-23 at 16.50.39.png

  1. 通常のブレークポイントの作成
  2. 青ラベルをダブルクリックする
  3. 条件フィールドに条件を入力する。ここでは cat.name == "ネコノヒー" と入力しました。

表示変数

ブレークポイントに来たらアクションを実行して、プログラムを停止しないこともできます。

print(cat.name)

上記コードは次のように置き替えることもできます。

ezgif-2-3a86395ceed2.gif

Screen Shot 2020-05-23 at 16.49.47.png

  1. 通常のブレークポイントを作る
  2. Add Action ボタンをクリックする
  3. po cat.name などのアクションを入力する。po がどのようなものであるかについては、次のパートで取り上げます。
  4. トグルスイッチを、評価アクション後に自動的に継続するオプションに合わせる Automatically continue after evaluating actions

ブレークポイントに来てもプログラムは停止せず、猫の名前がコンソールに表示されます。

Screen Shot 2020-05-23 at 16.57.56.png

Xcodeが組み込まれたランタイムデバッガ (po)

ブレークポイントでプログラムが停止した時は、変数の値を調べて、変数値を変えて下さい。

例えばここにブレークポイントを置くことができます。

Screen Shot 2020-05-23 at 16.54.52.png

ブレークポイントでプログラムが停止した時は、コンソールウィンドウで命令を実行できます。

変数を調べることができます。

ここでは、po は表示オブジェクトにあたります。

po myCats

ezgif-2-5c651f6b6d79.gif

po myCats.count

ezgif-2-4a6e311fe684.gif

po myCats.first?.name

ezgif-2-4f432462041b.gif

変数を変更することも可能です

po myCats.count //2
po myCats.append(.init())
po myCats.count //3

ezgif-2-74bf7e847d7a.gif

Xcode オーガナイザー

App Storeで既にプログラムが公開されていれば、Appleがクラッシュの情報を集めてくれます。

Xcode のトップメニューで、"Window"と"Organizer"をクリックしてください。

Screen Shot 2020-05-23 at 17.10.37.png

左側でApp Storeで既に公開しているアプリケーションをクリックして、トップの"Crashes"をクリックしてください。

Screen Shot 2020-05-23 at 17.12.09.png

App Storeにある私のアプリで起きた実際のクラッシュに関するレポートをシェアしたいと思います。この問題はすでに解決済みです。

まずはバージョンのドロップダウンメニューをクリックしてバージョンを選択してください。

Screen Shot 2020-05-23 at 17.14.16.png

実際に起きたソフトウェアのクラッシュのリストが表示されるかと思います。そのうちの1つをクリックして詳細を見てください。

Screen Shot 2020-05-23 at 17.15.17.png

青色の "Open in Project" (プロジェクト内で開く) ボタンをクリックすると、自分のプロジェクトファイルを選択でき、どのラインでクラッシュがあったのかを教えてくれます。

Metrics

これまでの記事で話したように、メトリクスをチェックしてメモリに問題がないかどうかを確認することもお勧めします。

iOSアプリのメモリリーク (Memory Leak) の検出とデバッグの方法を、ネコに関するプログラムで学びましょう。(Reference Cycle 参照サイクル)

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

【Swift】位置情報を取得する

概要

swiftで位置情報を取得する方法について、まとめました。

startUpdatingLocation() で位置情報の取得を開始し、取得した場合 func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) が呼び出され位置情報を確認することだできます。

class ViewController: UIViewController, CLLocationManagerDelegate {

      var locationManager : CLLocationManager?

      override func viewDidLoad() {
            super.viewDidLoad()

            locationManager = CLLocationManager()
            locationManager!.delegate = self

            //位置情報を使用可能か
            if CLLocationManager.locationServicesEnabled() {

                 //位置情報の取得開始
                 locationManager!.startUpdatingLocation()

            }
     }

     // 位置情報を取得した場合
     func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

     }
}

info.listに追記

位置情報を取得する場合、info.list に「Location Always and When In Use Usage Description」、「Location When In Use Usage Description」を追加し位置情報どのように使用するか記載する必要があります。

info.list
// ユーザーの位置情報へのアクセスを常に要求している理由を記載します。
Privacy - Location Always and When In Use Usage Description

// フォアグラウンドで実行されているときに、アプリがユーザーの位置情報へのアクセスを要求する理由を記載します。
Privacy - Location When In Use Usage Description

位置情報を取得する際の設定

位置情報を取得する際に、精度や取得間隔の指定ができます。

locationManager = CLLocationManager()
locationManager!.delegate = self

// 常に使用する場合、バックグラウンドでも取得するようにする
if CLLocationManager.authorizationStatus() == .authorizedAlways {
     // バックグラウンドでも取得する
     locationManager!.allowsBackgroundLocationUpdates = true
} else {
     // バックグラウンドでは取得しない
     locationManager!.allowsBackgroundLocationUpdates = false
}

// 位置情報の取得精度を指定します
locationManager!.desiredAccuracy = kCLLocationAccuracyBest

// 更新に必要な最小移動距離
// Int値を指定することで、○○m間隔で取得するようになります
locationManager!.distanceFilter = 10

// 移動手段を指定します
// 徒歩、自動車等
locationManager!.activityType = .fitness

// 位置情報取得開始
locationManager!.startUpdatingHeading()

CLLocationManager.authorizationStatusの設定値

authorizationStatus 設定
.notDetermined 未設定
.restricted 機能が制限されている
.denied 許可しない
.authorizedWhenInUse このAppの使用中のみ許可
.authorizedAlways 常に許可

activityTypeの設置値

activityType 移動手段
.fitness 徒歩
.automotiveNavigation 自動車
.other その他

desiredAccuracyの設置値

desiredAccuracy 精度
kCLLocationAccuracyBestForNavigation デフォルト
kCLLocationAccuracyBest 最高精度
kCLLocationAccuracyNearestTenMeters 10m以内
kCLLocationAccuracyHundredMeters 100m以内
kCLLocationAccuracyKilometer 1km以内
kCLLocationAccuracyThreeKilometers 3km以内

位置情報を取得した場合

位置情報を取得した場合以下の、関数を呼び出し緯度経度等の情報を取得できます。

// 位置情報取得した場合
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

     guard let newLocation = locations.last else {
          return
     }

     let location:CLLocationCoordinate2D
            = CLLocationCoordinate2DMake(newLocation.coordinate.latitude, newLocation.coordinate.longitude)

     let formatter: DateFormatter = DateFormatter()
     formatter.timeZone   = TimeZone(identifier: "Asia/Tokyo")
     formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
     let date = formatter.string(from: newLocation.timestamp)

   print("緯度:", location.latitude, "経度:", location.longitude, "時間:", date)

}

参考

CLLocationManager

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

【SwiftUI】ビューをジェスチャーで回転させ、90°ずつスナップを効かせる方法

【SwiftUI】ビューをジェスチャーで回転させ、90°ずつスナップを効かせる方法

こんにちは、たいちです(@taichi_swift_)

たかし「お絵かきアプリとかでよくある、ビューを回転させて90°づつピタって止めるやつ、どうやって実装するんだろ...」

こちらのお悩みを解決します。

ビューを回転させる方法

まずは、基本のビューを回転させる方法です。
SwiftUIでは、
.rotationEffect(_ angle: Angle, anchor: UnitPoint = .center)
というmodifierを使用します。

実際に使うときには、
.rotationEffect(Angle(degrees: 90))
みたいに使うわけですね。

IMG_1985 2.jpg

Text("RotationEffect")
    .rotationEffect(Angle(degrees: 90))

ビューをジェスチャーで回転させる方法

続いて、ユーザーが指を使ってビューを回転させる方法です。
.gesture()
を使います。

まずは、角度を格納する変数を2つ用意します。

@State var angle = Angle(degrees: 0.0)
@State private var oldAngle = Angle(degrees: 0.0)

そして、ジェスチャーを用意します

var rotation: some Gesture {
    RotationGesture()
        .onChanged { angle in
            self.angle = self.oldAngle + angle                
        }
        .onEnded { angle in
            self.oldAngle = self.angle
        }
    }

そんでこのジェスチャーをテキトーなビューにがっちゃんこします。
.rotationEffect(self.angle)
.gesture(rotation)

RoundedRectangle(cornerRadius: 20)
    .frame(width: 400, height: 300)
    .rotationEffect(self.angle)
    .gesture(rotation)

すると、二本指で自由に操れるビューができました。
IMG_1986.jpg

ビューを回転させ、スナップさせる方法

やっときました。本題です。

先ほど定義した、rotationという名前のgestureにちょこっと細工を加えてあげます。

var rotation: some Gesture {
    RotationGesture()
        .onChanged { angle in
            self.angle = self.oldAngle + angle
            if abs(Int(self.angle.degrees) % 90) < 5 {
                self.angle = Angle(degrees: Double(Int(self.angle.degrees) - Int(self.angle.degrees) % 90))
            }
        }
        .onEnded { angle in
            self.oldAngle = self.angle
        }
}

完成です。

struct ContentView: View {
    @State var angle = Angle(degrees: 0.0)
    @State private var oldAngle = Angle(degrees: 0.0)

    var rotation: some Gesture {
        RotationGesture()
            .onChanged { angle in
                self.angle = self.oldAngle + angle
                if abs(Int(self.angle.degrees) % 90) < 5 {
                    self.angle = Angle(degrees: Double(Int(self.angle.degrees) - Int(self.angle.degrees) % 90))
                }                
            }
            .onEnded { angle in
                self.oldAngle = self.angle
            }
        }
        var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .frame(width: 400, height: 300)
            .rotationEffect(self.angle)
            .gesture(rotation)        
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】基本的なモディファイアの種類と使い方②

テキストの装飾に関するモディファイア

lineLimitモディファイア

文字列の表示行数の最大値を設定
例:文字列"hogehoge"を20回繰り返して3行に収める

Text(String(repeating:"hogehoge",
            count:20))
    .lineLimit(3)

truncationModeモディファイア

文字列を省略する場所を指定
例:文字列の中心が省略

Text(String(repeating:"hogehoge",
            count:20))
    .lineLimit(1)
    .truncationMode(.middle)

lineSpacingモディファイア

行間の幅を指定 
cssのline-heightと同じ

Text("hogehoge")
    .lineSpacing(50)

fontモディファイア

フォントの種類を指定
種類:
largeTitle:大きなタイトル
title:タイトル
headline:見出し
subheadline:小見出し
body:本文
callout:吹き出し
footnote:注釈
caption:キャプション

Text("hoge")
    .font(.body)

boldモディファイア

文字を太字にする

italicモディファイア

文字を斜体にする

fontWeightモディファイア

文字の太さを設定する
(太い順にblack,heavy,bold,semibole,medium,regular,light,thin,ultraLight)

baseLineOffsetモディファイア

ベースラインの太さを設定
正の数字ベースラインが上がる

kerningモディファイア

2文字間の間隔を設定

trackingモディファイア

文字列全体の文字間隔を調整
kerningより優先される

underlineモディファイア

文字列にアンダーラインを表示
active:下線を表示するかどうか
color:下線の色

strikethroughモディファイア

文字列に取り消し線を表示
active:取り消し線を表示するかどうか
color:取り消し線の色

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