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

【SwiftUI】Listで削除Alertをだす

SwiftUIでリストの削除時にアラートを出す方法です。

こうして

iPhone_11_—_13_3.png

こうするやつです

iPhone_11_—_13_3.png

ポイント

  1. onDeleteメソッドを使いますが、ListではなくForEachのメソッドなのでForEachを使います。
  2. alertモディファイアを使いますが、isPresentedではなく、itemを使います。(何こ目の要素かを取得するため)
  3. itemIdentifiableです。
import SwiftUI

struct ContentView: View {
    @State var deleteAlert:Bool = false
    @State var item: Item?

    func showDeleteAlert(offsets: IndexSet) {
        self.item = Item(id: String(offsets.first!))
    }

    struct Item: Identifiable {
        var id:String
    }

    var body: some View {
        List {
            ForEach(0..<5, id: \.self) { id in
                Text(String(id))
            }
            .onDelete(perform: showDeleteAlert)
            .alert(item: self.$item) { i in
                Alert(title: Text(i.id + "を削除しますか?"),
                      primaryButton: .destructive(Text("Delete")) {
                        // deleteの処理
                    },
                      secondaryButton: .cancel() {
                    }
                )
            }
        }
    }
}

参考

https://qiita.com/1amageek/items/e90e1cfb0ad497e8b27a
https://fuckingswiftui.com/#alert
https://www.hackingwithswift.com/books/ios-swiftui/using-alert-and-sheet-with-optionals

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

Xcodeプロジェクトで Swift Package Manager を使っているとBitriseでfastlaneのテスト実行が失敗する問題に対処

事象

Xcodeプロジェクトを Swift Package Manager を使ってライブラリ管理するように変更したところ、Bitriseで以下のようなエラーが出るようになりました。

xcodebuild: error: Could not resolve package dependencies:
An unknown error occurred. '/Users/vagrant/Library/Developer/Xcode/DerivedData/Timetodo-cdnlkhtafdnmclauocshwxxtmoag/SourcePackages/repositories/XCGLogger-9081359c' exists and is not an empty directory (-4)
timeout after 600 seconds

解決方法

Fastfileに以下を追加してライブラリの依存解決すると成功するようになりました。

    Dir.chdir("../") do
      sh("xcodebuild", "-resolvePackageDependencies")
    end

参考

https://errors.wtf/xcodebuild-error-could-not-resolve-package-dependencies/

環境

Xcode 11.3.1
Swift 5.1.3

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

Instance method 'request(:didFailWithError:)' nearly matches optional requirement 'request(:didFailWithError:)' of protocol 'SKRequestDelegate' に対処する

事象

SKProductsRequestDelegate の継承元である SKRequestDelegate に定義されている以下のメソッドをSwiftで実装してエラーハンドリングしようとしたところ、warningが出てメソッドが呼ばれない事象に遭遇しました。

func request(_ request: SKRequest, didFailWithError error: Error) {
}

Instance method 'request(:didFailWithError:)' nearly matches optional requirement 'request(:didFailWithError:)' of protocol 'SKRequestDelegate'

解決方法

以下のように @objc でメソッド名を指定し、errorの型を NSError に変更することでメソッドが呼ばれるようになりました。

@objc(request:didFailWithError:)
func request(_ request: SKRequest, didFailWithError error: NSError) {
}

未解決

上記のように変更してもwarningは消えませんでした...

参考

https://stackoverflow.com/questions/39416385/swift-3-objc-optional-protocol-method-not-called-in-subclass

環境

Xcode 11.3.1
Swift 5.1.3

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

【Swift クイズ】そのクロージャ、本当に [weak self] 必要ですか?

この記事は何?

クロージャにおいて [weak self] は本当に必要なのか?
実際のところケースバイケースですが、今回は DispatchQueue.main を例にしてクイズ・解説をしたいと思います。
ということで、早速クイズです。

環境

  • Xcode Version 11.3.1 (11C504)

クイズ

第一問

次のコードに [weak self] は必要ですか?

DispatchQueue.main.async { [weak self] in
    guard let `self` = self else { return }
    // self 使った処理
}

第二問

次のコードに [weak self] は必要ですか?

UIView.animate(withDuration: 10) { [weak self] in
    guard let `self` = self else { return }
    // self 使った処理
}

いかがでしょうか?
特段の理由なく [weak self] を記述しているのであれば、引き続きクイズ回答・解説をご覧ください。

クイズ回答

第一問

A. [weak self] は必須ではない。処理の内容によっては [weak self] が妥当な場合もある。

第二問

A. [weak self] は不要。

解説

一般的な話として、循環参照を回避する目的で [weak self] を指定する場合がありますが、
上記の処理は、クロージャ実行後に循環参照が解決されるので [weak self] は必須ではありません。

かんたんな実験によって、このことを確認することができます。

class Detail1ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        DispatchQueue.main.async { [object = SomeObject()] in
            object.doSomething()
        }
        // あるいはこういうコード
//        UIView.animate(withDuration: 30) { [object = SomeObject()] in
//            object.doSomething()
//        }
    }
}

ここで、SomeObject には動作確認のための実装をしておきます。

class SomeObject {
    init() { log() }
    func doSomething() { log() }
    deinit { log() }

    private func log(_ function: String = #function) {
        print("???\t\(Self.self).\(function)")
    }
}

そうすることで、実行時には次のコンソール出力を確認することができます。

???    SomeObject.init()
???    SomeObject.doSomething()
???    SomeObject.deinit

doSomething() の実行後に deinit が呼ばれることが確認できると思います。

SomeObject を強参照しているのはクロージャだけなので、
これは、クロージャの実行後にクロージャ自体がメモリから開放されたことを意味しています。

つまり、DispatchQueueUIView.animate[weak self] せずとも、循環参照によるメモリリークは発生しないことを意味しています。
なので、循環参照の回避を目的とした weak キャプチャは不要です。

ただし、非同期処理の実行時にオブジェクトが開放されていても良い場合や、
開放されていることが妥当な場合は、[weak self] を指定することが好ましいと思います。

たとえば、DispatchQueue...asyncAfter が実行される前に、ナビゲーションコントローラから該当のビューコントローラがポップされた場合を考えてみます。
この場合は、次のコードのように [weak self][weak view = view] のような指定を行うことで、無意味な処理をスキップすることができます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // 強参照によるキャプチャを行った場合、
        // クロージャの実行が完了するまでビューコントローラが開放されることはありません。
        // ビューコントローラをポップしたあとはビューを操作しても意味が無いので weak キャプチャが妥当です。
        DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
            self.view?.backgroundColor = .orange
        }
    }
}

DispatchQueue のように、非同期処理の実行後にクロージャが破棄される処理では、次のことに気をつけましょう。

  • weak キャプチャせずとも循環参照は発生しない
  • 必要に応じて weak キャプチャを使うことで無意味な処理をスキップできる

一方で UIView.animate については、単純にキャプチャリストは不要です。
クロージャ実行後、クロージャを破棄する点では DispatchQueue と同じですが、実はクロージャを同期処理として実行するので、weak キャプチャは不要です。

また、UIView.AnimationOptions.repeat などを指定してもクロージャの実行は1回きりです。

かんたんな実験によって、この事実の動作確認ができます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // `animations` で指定したクロージャは即実行されるので、weak キャプチャは無意味。
        //
        // `completion` で指定したクロージャは、`animations` で指定したアニメーションが停止したタイミングで実行される
        // 例えば、次のいずれかの処理を行うと `completion` がコールされる。
        //     - ビューコントローラをナビゲーションコントローラからポップする
        //     - view.layer.removeAllAnimations() をコールする
        //
        // `animations`, `completion` ともに実行は一度きりで、
        // 実行後にクロージャは開放される
        UIView.animate(
            withDuration: 10,
            delay: 0,
            options: .repeat,
            animations: { self.view?.backgroundColor = .orange },
            completion: { [o = SomeObject()] _ in o.doSomething() }
        )
    }

    deinit {
        print("???\t\(Self.self).\(#function)")
    }
}

このように [weak self] の無いコードを書いても、ナビゲーションコントローラから該当のビューコントローラをポップすることで、ビューコントローラの deinit が呼ばれてメモリリークしないことが確認できます。

まとめ

非同期処理のコールバックハンドラとしてクロージャを扱う場合でも、クロージャの実行後に、そのクロージャがメモリから開放されれば、循環参照が解決されたことになるのでメモリリークは発生しません。

そして、 DispatchQueue は上記に該当するので、[weak self] がなくてもメモリリークしません。[weak self] の有無は、循環参照が引き起こすメモリリーク問題とは切り離して、処理のスキップが必要かどうかで判断しましょう。

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

SocialHub を支える技術 - iOS Android 共通ライブラリ編 -

SocialHub とは?

 まずはアプリの紹介をさせてください。SocialHub とは、先日筆者がリリースした iOS 向けマルチ SNS クライアントアプリです。大きな特徴として挙げられるのが、複数の SNS を同時に見て投稿できる ことで、現在対応している SNS の種類は Twitter, Mastodon, Slack, Tumblr になります。

SocialHub - マルチ SNS クライアント - | App Store

マルチプラットフォーム

 SocialHub は現在 iOS のアプリしかリリースしていませんが、Android のアプリも見据えたプロジェクトで、複数の SNS を統合して扱う都合上複雑なロジックから逃げられないため、できればワンコードでロジックを記述したい気持ちがありました。昨今流行りのマルチプラットフォームなフレームワークを用いての実装を、当初 (2018年初旬) 考えており、当時のマルチプラットフォームというと、XamarinReact Native が選択肢としてあったのですが、自分がその分野に明るくなかった事や、一番の問題として、そのライブラリで本当に自分の目指した UI/UX が実現できるか自信がなかったことです。

 また、技術選定中に Flutter も注目を浴びましたが、こちらについてもサンプルプロジェクトを作って動作を検証したのですが、パフォーマンスが気になる事や、情報の少なさから、自分の求める UI が作成できるか自信がなかったので採用を見送りました。

共有ライブラリという選択

 そこで注目したのが、iOS/Android で共有ライブラリを作ることのできるフレームワークです。共有ライブラリを用いることで、共通のロジックは全て単一の言語で記述し、クライアントアプリ側はネイティブで UI を記述することによって、リッチな体験を担保します。以下に選択肢として考えたものについて記述します。(理解が浅い部分があったらコメントくださると助かります)

Embeddinator-4000


 共有ライブラリという方針を決めてから、一番初めに見つけたのが Embeddinator-4000 です。これは Xamarin のサブプロジェクトの一つで C# で書かれたコードを iOS/Android 向けの共有ライブラリとして扱うことができるようにするコンパイラ? (適切な表現かどうか不明) です。細かい内容については、以下の自分のブログ記事を御覧ください。

 .NET のコードを他言語に変換? Embeddinator-4000 を使ってみた。| urushi blog

 動作確認までしたのですが、正直 iOS 向けにバイナリをビルドする際に、C# のジェネリクスが使えない事がかなりの痛手で、ほぼまともに動作するライブラリを作成することができませんでした。理由としては iOS 向けに mono のランタイムをアプリに同梱することができないためで、一朝一夕でなんとかなる問題ではありませんでした。また、開発もあまり活発的ではなく、上記の問題の解決が期待できないので諦めざるを得ない状況でした。

J2ObjC


 次に発見したのが J2ObjC です。Google が主に開発を進めているプロジェクトで、Java のコードを Objective-C に変換してくれるコンパイラ になります。既に公開から八年以上経っているプロジェクトですが、まだ開発が行われており、Google 社内でも恐らく使われているようなので、安心感があります (?) J2ObjC についても、ブログ記事を書いたので、詳しくは以下を参照してください。

 Java のコードを Objective-C に? J2ObjC を試してみた。 | urushi blog

 SocialHub はこの J2ObjC を採用しました。 理由としては、

  • 自分に Java に対しての言語理解があること
  • プロジェクトがメンテされていること
  • パフォーマンスの劣化がほぼ無いこと

 が挙げられます。J2ObjC はコンパイル前後の言語 (要するに Java と Objective-C) の知識をある程度必要とするので、問題に対応する際に、Java が分からないでは対処が難しいケースが存在します。

 ということで SocialHub で作成したコアライブラリがこちら。

 SocialHub | github

 (ライブラリ名も SocialHub) J2ObjC でここまでコンパイルしている例って Google 外ではあまりないんじゃないかと思う。(そもそも J2ObjC なんて誰も使ってねえよ、っていう声は聞かないことにします) コンパイルは GitHub Actions に代行させており、大体所要時間は 15 分程。Java を Objective-C に変換した後、マルチアーキテクチャ向けにバイナリを作成し、Cocoapod のレポジトリを作成するところまで実行します。

 補足として、J2ObjC のサブプロジェクトでは、既にメンテナンスされていないものも存在し、ややエコシステムとして不便な部分もありました。J2ObjC はコンパイルする際に make (or bazel) を使うケースが多いのですが、自分はめんどくさがりなので、J2ObjC の Gradle Plugin を用いてコンパイルを行っていました。Plugin を使うと Gradle で記述されている依存関係についても自動的にコンパイルターゲット加え、かつ import での参照関係も確認してくれるため、余計なコンパイルをしないで済みます。しかし Gradle Plugin は既にメンテされなくなっており、Java11 では動作しない問題を抱えていました。そのため Fork して修正しました。(結局手間がかかっている)

 正直、J2ObjC での初期の実装は茨の道でしたが、その道を通り抜けると、それなりに快適な開発環境を整えることができました。ということで、SocialHub では Java のライブラリを使用して iOS アプリを作成しています。

 アプリ内の権利表記にはコアライブラリ内で使用した Java のライブラリも記載しています。Twitter4J をはじめとした各種 SNS クライアントライブラリを使っています。

Kotlin/Native

 一応、共有ライブラリということなので、忘れてはいけないもう一つの選択肢である
Kotlin/Native について記載します。こちらは Kotlin で書かれたコードを LLVM エコシステムに混ぜてしまおうという、実にシンプルな目的のプロジェクトです。

 ここで厄介なのが、Kotlin で書かれたコードといっても、その内容に制約がかかることです。Kotlin といえば一般的に Java の資産がそのまま流用できるイメージを持たれていると思いますが、Kotlin/Native においては JVM 外での Kotlin の使用になるので java, javax 以下のパッケージのライブラリには使用の制限がかかりますし、既存の Kotlin ライブラリもマルチプラットフォーム用に記述されていないと使用できません。

 利点としては、プラットフォーム API 等を用いたより高度な抽象化ロジックを記述することができる事にあります。Kotlin/Native では iOS の UIKit を扱うためのライブラリ等が提供されているため、共有ライブラリのレベルで実現できる範囲が広いのが特徴です。SocialHub ではあまりその恩恵を受けられないので、J2ObjC を選択しました。将来的に Java のライブラリが困難なく使用できるレベルになってきたら使用するか検討したいと考えています。(そんな日が来るのかあんま詳しくない)

まとめ

  • マルチプラットフォームを選択する場合、本当に実現したい事が何かに立ち戻る

 SocialHub の場合ユーザー体験について妥協することがどうしても嫌だったので、UI やパフォーマンスに対してフレームワークを理由に妥協することがどうしてもできなかった → 共通ライブラリという選択

 無論、何を意識するかで全てが変わるので、実装スピードやスキルセットに合わせて Flutter や ReactNative を使用することは全然アリです。というか、かなり自分は異端だと感じています。

  • J2ObjC 使ってる人いないけどいいぞ

 共通ライブラリをまとめると以下のような印象。

フレームワーク 言語 導入難度 知名度 実用性
Embeddinator-4000 C# 難しい ほぼ無名
J2ObjC Java 難しい ほぼ無名
Kotlin/Native Kotlin 普通 有名

 J2ObjC のすごいところは、Java の標準ライブラリをほぼ Objective-C で再実装されている点で、思った以上に普通に Java がコンパイルできることです。でも Java なんだよなぁ。。。

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

Firebaseでお手軽サーバレスアプリ(Realtime Database+CloudFunctions+Cloud Messaging+Android(kotlin)+iOS(swift))その3

はじめに

今回はiOSからFirebaseのRealtime Databaseに接続しデータの登録と参照を行ってみます。
必要なのは、MacとXCodeです。
今回はMacBook Air (Retina, 13-inch, 2018)、XCode Version 11.3.1 (11C504)を使っています。

XCodeでアプリのベースを作成

  • Create new project を実行
  • iOSのSingle View Appを選択
  • Product nameを適当につけます
  • 言語はSwift、User InterFaceはStoryBoardを選択
  • 実行してスケルトンが動作することを確認します

Firebaseの準備

  • ブラウザでFirebaseのコンソールにアクセスし、iOSを追加します
    InkedScreen Shot 2020-02-24 at 9.48.30_LI.jpg
    InkedScreen Shot 2020-02-24 at 9.49.28_LI.jpg

  • iOSアプリにFireBaseを追加するで、iOSアプリのバンドルIDを入力します

  • バンドIDはプロジェクトを選択しGeneralのタブで参照できます

  • 設定ファイルをダウンロードせよと出るのでFireBaseのページで指示されるとおりに操作します

  • plistを追加できたら「次へ」を押します

  • cocoapodsが必要なので、入れてない場合はインストールします

  • terminalでプロジェクトのフォルダを開きます

  • terminalでの手順はFireBaseのページの通です

    • pod init
    • Podfilesの編集
    • pod install
  • xcodeを一旦閉じて「.xcworkspace」で開きます

  • AppDelegateにFirebaseへ接続するコードを追加します

AppleDelegate.swift
import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        FirebaseApp.configure()
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}
  • ここまで来たらビルドして実行して、firebaseのページでアクセスできたか確認します
  • firebaseがアプリに正常に追加されました と表示されればO K

Screen Shot 2020-02-24 at 10.16.11.png

iOSアプリにUIを追加

  • Main.stoyboardにTextViewを追加します
  • Constraintsは上左右は0、下は200
  • 下のスペースにButtonを追加します
  • Constrainsは上下左右5に設定します

Screen Shot 2020-02-24 at 10.27.54.png

DBへの書き込みを追加

  • ViewControllerにボタンを押した際のイベントハンドラーを追加
  • DBに書き込むコードを書きます
ViewController.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    var databaseRef: DatabaseReference!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
       databaseRef = Database.database().reference().child("/log")

    }

    @IBAction func WriteToDB(_ sender: Any) {

        view.endEditing(true)
        let MyTime:Int = Int(NSDate().timeIntervalSince1970*1000)
        let messageData = ["name": "user2(ios)", "message": "だれ?","timestamp":String(MyTime)]
        databaseRef.childByAutoId().setValue(messageData)

    }

}
  • FirebaseDatabaseが解決できないとエラーになると思います。PodsのprofileにFirebaseDatabaseを追加します。
  • Podsに追加しらPod installをします
profile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'otameshi-ios' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for otameshi-ios
pod 'Firebase/Analytics'
pod 'Firebase/Database'
end
  • ビルドして動作させます。ボタンを押してDBにデータが書き込まれれば成功です

データの取得と表示

  • データの更新前にTextViewを更新します
ViewContoller.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    var databaseRef: DatabaseReference!
    @IBOutlet weak var TextView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        databaseRef = Database.database().reference().child("/log")
        databaseRef.observe(.childAdded, with: { snapshot in
        if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"], let time = obj["timestamp"] {
            let currentText = self.TextView.text
            self.TextView.text = (currentText ?? "") + "\n \(time) \(name) : \(message)"
            }
        })
    }

    @IBAction func WriteToDB(_ sender: Any) {
        self.view.endEditing(true)
        let MyTime:Int = Int(NSDate().timeIntervalSince1970*1000)
        let messageData = ["name": "user2(ios)", "message": "だれ?","timestamp":String(MyTime)]
        self.databaseRef.childByAutoId().setValue(messageData)
    }

}

TextViewをオートスクロールに

  • 一番下まで行った時に勝手にスクロールするようにします
ViewContoller.swift
import UIKit
import FirebaseDatabase

class ViewController: UIViewController {

    var databaseRef: DatabaseReference!
    @IBOutlet weak var TextView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        databaseRef = Database.database().reference().child("/log")
        databaseRef.observe(.childAdded, with: { snapshot in
        if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"], let time = obj["timestamp"] {
            let currentText = self.TextView.text
            self.TextView.isScrollEnabled = false
            self.TextView.text = (currentText ?? "") + "\n \(time) \(name) : \(message)"
            self.scrollToBottom()
            }
        })
    }

    @IBAction func WriteToDB(_ sender: Any) {
        self.view.endEditing(true)
        let MyTime:Int = Int(NSDate().timeIntervalSince1970*1000)
        let messageData = ["name": "user2(ios)", "message": "だれ?","timestamp":String(MyTime)]
        self.databaseRef.childByAutoId().setValue(messageData)
    }

    func scrollToBottom() {
        TextView.selectedRange = NSRange(location: TextView.text.count, length: 0)
        TextView.isScrollEnabled = true

        let scrollY = TextView.contentSize.height - TextView.bounds.height
        let scrollPoint = CGPoint(x: 0, y: scrollY > 0 ? scrollY : 0)
        TextView.setContentOffset(scrollPoint, animated: true)
    }    
}

まとめ

これでAndroidとiOSのアプリが揃いました。
それぞれでボタンを押すと同時に反映されることが確認できると思います。

Screen Shot 2020-02-24 at 12.06.01.png

このシリーズについて

その1:全体像とFirebaseの準備

その2:Androidのクライアントアプリからデータの登録と表示まで

その3:iOSのクライアントアプリからデータの登録と表示まで

[その4:準備中]

[その5:準備中]

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

お絵描きアプリで良い感じの線を引く今時の方法を調べた

はじめに

昨年、自作のお絵かきゲームアプリをリリースしました。
アナリティクスを見ると、残念ながら当初私が考えていたようなゲーム用途としては全く手応えなしだったんですが、
デッサン練習用途としては、少しだけ需要が感じられる状態でございます。
そこで、使っていただいているユーザーの方からapple pencilで線が途切れるとの情報を寄せていただいたので、この機会に、線の引き方について再度調べてみました。

これまで線の引き方については、

KikurageChan様の記事
iOS標準機能の良いお絵描きアプリを目指して・・・
hollymoto様の外部記事
https://anthrgrnwrld.hatenablog.com/entry/2016/07/14/230929

を参考にさせていただき、UIBezierPathを使いタッチ開始/終了の中点を取ってベジェ曲線を描いていたのですが、(紹介されている中では手軽なやり方)
傾けても使えるようになっているapple pencilでは、タッチを拾えないときがあると分かりました。

合体タッチ

調査の結果、Swift 9.0からは合体タッチ(coalescedTouches)と呼ばれる機能が存在し、この機能を使えばapple pencilの傾きを検知し、かつ、滑らかに線が引けることが分かりました。
従来の方法ではタッチイベントのストロークごとに始点と終点が取れるので、間に直線を引くか、中点を自前で計算して、ベジェ曲線を引いていました。
合体タッチではストロークごとのタッチを配列に持ち、複数の制御点を使って線を引ける仕組みであるようです。

1401233.png▶️1401233.png

合体タッチを使うと、特別なことをしなくても複数の制御点が手に入るので、滑らかな線が引けそうですね。
ただ、iOSデバイスのタッチスキャン性能に影響されます。またやはり、指よりもapple pencilの方が滑らかです。
一応、iPhone 5sで指で描くことを試してみても、そこまで角張った線にはならなかったので、
iOS9.0以上が使えるデバイスであれば、採用を検討できるのではないかと思います。

サンプルコード

    private func drawStroke(context: CGContext, touch: UITouch) {

        let previousLocation = touch.previousLocation(in: self)
        let location = touch.location(in: self)

        drawColor.setStroke()
        context.setLineWidth(lineWidth)
        context.setLineCap(.round)
        context.move(to: CGPoint(x: previousLocation.x, y: previousLocation.y))
        context.addLine(to: CGPoint(x: location.x, y: location.y))
        context.strokePath()

    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        guard let canvas = self.imageView.image else {
            return
        }
        UIGraphicsBeginImageContextWithOptions(canvas.size, false, 0.0)
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }

        let imgCanvasRect = CGRect(x: 0, y: 0, width: canvas.size.width, height: canvas.size.height)

        tmpImage?.draw(in: imgCanvasRect)

        var touches = [UITouch]()
        //合体タッチ取得
        if let coalescedTouches = event?.coalescedTouches(for: touch) {
            touches = coalescedTouches
        } else {
            touches.append(touch)
        }
        for touch in touches {
            drawStroke(context: context, touch: touch)
        }

        //合体タッチで描いた画像の取得
        tmpImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return tmpImage
    }

本来、タッチイベントはUIImageView上で取得したかったのですが、
iOS13.0からのデフォルトであるSwiftUIを雛形にするプロジェクトでは
AppDelegateからViewControllerを起動したとき、
なぜかUIImageView上でのtouchesMovedイベントが発生しない状態になりました。。
UIView上でならば、上記で動くことを確認しました。
しかし毎回大きく変わるなぁ・・・。:sweat:

予測タッチ

正直、自分ではドキュメントを見ても仕組みがわからないのですが、
iOS9.0以降、予測タッチという機能が使えるようです。
タッチに対する画面レンダリングの遅延をカバーするため、
デバイスに蓄積された予測データに基づき、まだタッチされていない箇所に、あらかじめ線を引いているらしいです。
描く速度が早すぎて描画できないという事態を防ぐ手段と理解しました。

予測タッチを採用する場合、タッチのレンダリングが正しく行われた場合は
予測分の描画を破棄する必要が出てきます。
次のサンプルコードでは、合体タッチの線 + 予測タッチの線を画像として保存しておき、
タッチイベントが完了した時は合体タッチの線のみの画像で上書きすることで予測の破棄を行っています。

  override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        guard let canvas = self.imageView.image else {
            return
        }
        UIGraphicsBeginImageContextWithOptions(canvas.size, false, 0.0)
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }

        let imgCanvasRect = CGRect(x: 0, y: 0, width: canvas.size.width, height: canvas.size.height)

        tmpImage?.draw(in: imgCanvasRect)

        var touches = [UITouch]()
        if let coalescedTouches = event?.coalescedTouches(for: touch) {
            touches = coalescedTouches
        } else {
            touches.append(touch)
        }
        for touch in touches {
            drawStroke(context: context, touch: touch)
        }
        //合体タッチで描いた画像の取得
        tmpImage = UIGraphicsGetImageFromCurrentImageContext()
        //予測タッチ取得
        if let predictedTouches = event?.predictedTouches(for: touch) {
          for touch in predictedTouches {
            drawStroke(context: context, touch: touch)
          }
        }
        //合体タッチ + 予測タッチで描いた画像の取得
        self.imageView.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        //タッチイベント完了時、合体タッチのみの画像で上書きする
        self.imageView.image = self.tmpImage
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        //タッチイベントキャンセル時、合体タッチのみの画像で上書きする
        self.imageView.image = self.tmpImage
    }

サンプルコード・解説ともにほとんど
https://www.raywenderlich.com/121834/apple-pencil-tutorial
の翻訳なので、さらに詳しく知りたい方はこちらをご確認ください。

UIBezierPathも使ってみる

合体タッチ・予測タッチとも、制御点同士の間の線を直線で描いています。
制御点が多く取れる場合は良いですが、少ないと角ばる可能性が出てきます。
そこで、制御点同士の間の線をcontext.addLineの代わりに、UIBezierPathで描いてみるのを試してみました。コードは後述のGitHubにあります。
動かしてiPhone5s上で幾つも丸を描きましたが、私の絵心がないせいか、違いはほとんど分かりませんでした。
ただ理屈上はわずかでも滑らかになっているはずで、今後自分のアプリでも使ってみたいと思います。

PencilKitだ…と?

ここまで書いて何ですが、iOS13.0からはPencilKitというものが導入されていて、
iOSメモ帳で使えるペンのパレットツールが、簡単に実装できるようです。

niwasawa様の記事
たった3行のコードで PencilKit を導入して Apple Pencil 対応

これから作るアプリとかこれで良さそう。。
まだ自分で使っていないので分からないのですが、もしカスタイマイズが難しいのであれば
本記事の内容も場面によっては役に立てるでしょうか。

おわりに(宣伝)

お絵かきに興味のある方、宜しければ冒頭のアプリ、日々のデッサン練習に使ってみてください。
絵が上手くなって第二の鬼滅の刃描いちゃってください!
私としても、まだもう少し使い勝手を向上させて行きたいと思います。

記事内のサンプルコードをGitHubにあげました。
参考にして頂けましたら幸いです。

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

【Swift、Objective-c】画面遷移とメモリ使用について

Swift、Objective-cでの画面遷移をする際の遷移方法、メモリ使用についてまとめました。

画面遷移する方法

ViewController間での画面遷移は、storyboard ID、Segue Identifierを使用する方法と
dismissViewControllerAnimatedを使用する方法があります。

Objective-c
//storyboard IDを使用した画面遷移
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:@"storyboard ID"];
[self presentViewController:vc animated:NO completion:nil];

//Segue Identifierを使用した画面遷移
[self performSegueWithIdentifier:@"Segue Identifier" sender:self];

//dismissViewControllerAnimatedを使用した画面遷移
[self dismissViewControllerAnimated:NO completion:nil];
Swift
//storyboard IDを使用した画面遷移
let secondViewController = self.storyboard?.instantiateViewController(withIdentifier: "storyboard ID") as! nextViewController
        self.present(secondViewController, animated: true, completion: nil)

//Segue Identifierを使用した画面遷移
self.performSegue(withIdentifier: "Segue Identifier", sender: self)

//dismissViewControllerAnimatedを使用した画面遷移
self.dismiss(animated: false, completion: nil)

画面遷移する場合のメモリ使用量

2つのViewController間を画面遷移した場合、メモリ使用量の変化は以下のようになります。

  • dismissViewControllerAnimatedで画面遷移をした場合のメモリ使用量
    画面遷移した時に、メモリが解放されます。
    dismiss画面遷移.jpg

  • storyboard ID、Segue Identifierで画面遷移をした場合のメモリ使用量
    画面遷移をした時に新しく画面を作成していくため、画面遷移してもメモリが解放されず、
    メモリが溜まっていきます。
    present画面遷移.jpg

dismissViewControllerAnimated以外での画面遷移では新しく画面を作成する。
そのため、storyboard ID、Segue Identifierのみで画面遷移を続けた場合、メモリが解放されず溜まっていき、
結果として動作が重くなりアプリが強制終了してしまう。
そのため、新しい画面への画面遷移以外は、dismissViewControllerAnimatedで戻った方が良い。

dismissViewControllerAnimatedで遷移した場合の問題点

メモリ解放のため、dismissViewControllerAnimatedを使用した画面遷移を使用していきたいが、dismissViewControllerAnimatedは、現在表示されているViewControllerを削除することで画面遷移するため、戻った後のViewControllerは更新されない。

そのため、TableViewの画面情報を再表示したい場合、追加の処理を行う必要がある。

NSNotificationを使用したTableViewの更新

解決策として事前にmainViewControllerでNSNotificationの通知要求を登録しておき、遷移後のViewControllerから画面遷移する時に、通知を送ることで画面遷移後にTableViewを更新することができます。

Objective-c mainViewController
- (void)viewDidLoad {
     [super viewDidLoad];

     //通知要求の登録をする
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hoge:) name:@"reload" object:nil];
}

//通知を受信した時に実行する
-(void)hoge:(NSNotification *)notification {
     if([[notification name]isEqual:@"reload"]) {
          //TableViewの更新処理
          [tableView reloadData];
     }
}
Objective-c nextViewController
//画面遷移後に通知を送信する
[self dismissViewControllerAnimated:NO completion:^{
     [[NSNotificationCenter defaultCenter]postNotificationName:@"reload" object:self];
}];
Swift mainViewController
override func viewDidLoad() {
     super.viewDidLoad()

     //通知要求の登録をする
     let nc = NotificationCenter.default
     nc.addObserver(self, selector: #selector(hoge), name: Notification.Name(rawValue:"reload"), object: nil)
}

//通知を受信した時に実行する
@objc func hoge(notification: Notification?) {
     if notification!.name.rawValue == "reload" {
          tableView.reloadData()
     }
}
Swift nextViewController
//画面遷移後に通知を送信する
self.dismiss(animated: false, completion: {
     NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reload"), object: nil)
})

mainViewControllerまで一気に戻る場合

複数画面遷移している状態から一気にmainViewControllerまで戻る場合以下のようにすれば良い。

Objective-c
[[UIApplication sharedApplication].windows[0].rootViewController dismissViewControllerAnimated:NO completion:nil];
Swift
let window = UIApplication.shared.windows[0].rootViewController
window?.dismiss(animated: false, completion: nil)

参考

presentViewController:animated:completion:
dismissViewControllerAnimated:completion:
https://tutorialmore.com/questions-2605800.htm

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

【SwiftUI】TextFieldに初期値を入れる

initではなくて.onAppearで初期値を代入する

struct ContentView: View {
    @State var text:String = ""

    var body: some View {
        TextField("", text: $text)
        .onAppear {
            // ここで初期値を代入
            self.text = "default text"
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む