20220318のSwiftに関する記事は3件です。

[SwiftUI + ProgressView] 処理の進捗に応じて ProgressView を更新

この記事の内容 ・処理の進捗に応じて ProgressView を更新 ※ SwiftUI の ProgressView を利用 開発環境 ハードウエア 項目 内容 PC MacBook Air(M1,2020) メモリ:16GB ストレージ:1TB ソフトウエア 項目 内容 言語 Swift 5.6 IDE Xcode Ver 13.3 バージョン管理 GitHub 実際のコードは下記リンクからご確認いただけます この処理のイメージ(動画) シーケンス図 ポイント ・メインスレッドとは別のスレッドで時間がかかる処理を実行 ・上記処理の進捗をクロージャで取得 ・処理の途中に Start ボタンを押せないようにする コード DownloadManager クラス ・時間のかかる処理を実装している(※今回はダミーの処理を実装した) ・進捗をクロージャに渡す 注意 時間のかかる処理は、メインスレッドではなく別のスレッドで行うこと [理由] アプリがクラッシュするため DownloadManager.swift import Foundation class DownloadManager { static func startDownload(completion: @escaping (Int) -> Void){ print("start Download") var progress = 0 // 時間がかかる処理 DispatchQueue.global().asyncAfter(deadline: .now()){ while(progress <= 100){ print(progress) completion(progress) // 進捗をクロージャに渡す progress += progress < 90 ? Int.random(in: 1...10) : 1 sleep(1) } } } } 処理の途中に Start ボタンを押せないようにする 注意 今回のコードで時間のかかる処理でエラーが発生した場合(例:途中で処理が中断した等)を考慮していない。 ContentView.swift import SwiftUI struct ContentView: View { @State private var progress:Int = 0 @State private var canButtonTap:Bool = true var body: some View { VStack { Spacer() ProgressView("\(progress)%",value: Float(progress),total: Float(100)) .progressViewStyle(.linear) .padding() Button(action: tapButton,label: { Text("Start") }) .disabled(!canButtonTap) // ここでボタンの活性⇄非活性を切り替える .buttonStyle(.borderedProminent) Spacer() } } private func tapButton(){ self.canButtonTap = false // ボタンをタップしたら、ボタンを非活性にする ∵ 処理中のボタン2度押しを禁止するため DownloadManager.startDownload { progress in self.progress = progress // グロージャで渡らせれた進捗状況をビューに表示 self.toggleButton() } } private func toggleButton(){ if progress == 100 { canButtonTap = true // 進捗が 100 であればボタンを押せるようにする } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } 感想 ・クロージャの使い方が、やっと腑に落ちた 今後 ・今回の処理とファイルのダウンロード処理と連携したい ※URLSessionDownloadDelegate を利用すれば、ダウンロード処理の開始、終了及び進捗状況の更新ができそう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初めての Swift Package ライブラリーハンズオン

注 本記事は技育祭2022春の勉強会セッションで行われるハンズオンのための資料です。 前提条件 Xcode がインストールされています GitHub(GitLab 等でも可)のアカウントを持っています 上記アカウントへのプッシュ可能なローカル環境が構築されています ゴール おみくじを引くライブラリーを作り、Swift Package として GitHub で配布します 配布したライブラリーが正しく動作できるか、検証アプリで動作確認します 下準備 ライブラリー名を決めます 例:Omikuji ライブラリーの置き場を決めます 例:~/Documents/Omikuji(=/Users/MyAccount/Documents/Omikuji) Swift Package を作成 Xcode を開き、新規プロジェクトを作ります Multiplatform タブから Swift Package プロジェクトを選び、次に進みます 先ほど決めた置き場とライブラリー名で保存し、Create Git repository on my Mac オプションにチェックを入れます もし git の GUI クライアント(例:SourceTree や Fork)を利用しているなら、該当パスをクライアントに追加します(しなくてもターミナルから git 使えるので問題ありません、好みに応じて使い分けてください) 初期状態で作られたものを確認します Xcode に戻ると、Omikuji/Sources/Omikuji/Omikuji.swift というファイルがあるので、それをクリックすると初期状態で下記のようなコードがすでに書かれていることが確認できます: Omikuji.swift public struct Omikuji { public private(set) var text = "Hello, World!" public init() { } } 軽い動作確認 複数の Xcode で同じプロジェクトを開けないため、一旦ライブラリーを開いてる Xcode を閉じます ライブラリーを利用するアプリプロジェクトを作成します Xcode で新規 Multiplatform App プロジェクトを、ライブラリーの親フォルダーに作成します 例:~/Documents/OmikujiApp プロジェクト設定の Package Dependencies タブから、先ほど作ったライブラリーをローカルパスで追加します iOS もしくは macOS アプリターゲット(動作確認したいターゲットで OK です;両方入れても可)設定の General タブから、先ほど作ったライブラリーを Frameworks, Libraries, and Embedded Content に追加 Shared の ContentView.swift を開いて、ライブラリーの出力を確認します 先頭に import Omikuji を追加し、Text("Hello, world!") の部分を Text(Omikuji().text) で置き換え、プレビュー画面が Hello, World! に変わったかどうか(小文字の w から大文字の W に変わったかどうか)を確認します ContentView.swift import SwiftUI +import Omikuji struct ContentView: View { var body: some View { - Text("Hello, world") + Text(Omikuji().text) .padding() } } ライブラリーの機能を実装 動作確認アプリから直接 Omikuji パッケージの編集ができるので、そのまま Packages/Omikuji/Sources/Omikuji を開きます せっかく Swift は日本語プログラミングに対応しているので、日本語で実装しようと思います。というわけで既存の Omikuji.swift を削除し、新たに おみくじ.swift ファイルを作ります おみくじですので、大吉 中吉 小吉 吉 末吉 凶 大凶 の enum を作ります ライブラリー外のモジュール、すなわち検証アプリやライブラリーを利用するアプリで利用するので、public で作る必要があります おみくじ.swift public enum おみくじ { case 大吉 case 中吉 case 小吉 case 吉 case 末吉 case 凶 case 大凶 } おみくじを引きたいので、引く() という静的メソッドを作ります Swift には全ての case を配列で羅列してくれるためのプロトコル CaseIterable と、配列からランダムな要素を取り出すためのメソッド randomElement() があるので、これらを活用して実装します おみくじ.swift -public enum おみくじ { +public enum おみくじ: CaseIterable { case 大吉 case 中吉 case 小吉 case 吉 case 末吉 case 凶 case 大凶 } + +extension おみくじ { + + public static func 引く() -> Self { + allCases.randomElement()! + } + +} 画面で引いた結果を表示したいので、おみくじ を String として、CustomStringConvertible に適合します おみくじ.swift -public enum おみくじ: CaseIterable { +public enum おみくじ: String, CaseIterable { // ... } // ... +extension おみくじ: CustomStringConvertible { + + public var description: String { + rawValue + } + +} 検証アプリの ContentView.swift に戻り、今の実装を確認してみます Text の内容を おみくじ.引く().description で置き換えれば OK です ContentView.swift struct ContentView: View { var body: some View { - Text(Omikuji().text) + Text(おみくじ.引く().description) .padding() } } 動作が無事確認できたら git にコミットします ターミナルから直接使っている場合は % git add . % git commit -m "初コミット" のように全ての差分をステージングし、初コミットなどのコミットメッセージでコミットすれば OK です。コミットメッセージの内容は自由です。 また SourceTree や Fork を使っている場合は、それらのツールの使い方に従って全ての差分をコミットします。 OSS として公開 GitHub(もしくは GitLab とかでも問題ありません)で新しい空リポジトリーを作成します 例: https://github.com/my-account/omikuji 作られたリモートリポジトリーをローカルの git リポジトリーに設定します ターミナルから git を利用している場合は % git remote add origin https://github.com/my-account/omikuji SourceTree などの GUI アプリの場合はアプリの使い方に従って設定します リポジトリーを GitHub に Push します ターミナルから git を利用している場合は % git push --set-upstream origin main SourceTree などの GUI アプリの場合はアプリの使い方に従って設定します GitHub から最新の main ブランチをリリースします リリースバージョンはセマンティックバージョニングをお勧めします。それに従うと今回は初期開発リリースとしてバージョン 0.1.0 でリリースします。この際のタグは v0.1.0 とします 【オプショナル】SNS とかで自分が作ったライブラリーを宣伝します そりゃあみんなに使ってほしいですよね!? 作られたライブラリーを実際に利用 上に作った動作確認用のプロジェクトを開き、確認のためのローカルライブラリーの設定を削除 プロジェクト配下の Packages を丸ごと削除(元のデータを消したいわけではないので、Remove Reference で削除します) 再びプロジェクト設定の Package Dependencies タブから、公開 URL を入れてライブラリーを導入します もう一回 ContentView.swift を開いて動作確認します この際プレビューで表示されているおみくじの結果がおそらく変わります。もし変わらなかったら何回か別のファイルに切り替えてまた ContentView.swift ファイルに戻ってみてください。その度にプレビューが再生成され、新しいおみくじ結果が表示されるはずです 以上 これであなたも自分の Swift Package ライブラリーの配布ができるようになりました! また、今後は更に README.md を充実させてみるなどで、より人に使ってもらいやすいライブラリーに仕上げてみてもいいと思いますよ! 早くできた人へ 既にある程度の Swift 経験がある人なら今回のハンズオンはとてもスムーズに早くできちゃったかもしれません。というわけでそんなあなたに、とっておきな追加課題を出しましょう。実施は任意です。 今回のおみくじは完全にランダムであり、(乱数アルゴリズムのばらつきを無視すれば)どの結果でも同じ確率が出てきます。ところが実際に神社やお寺で引いたおみくじは(場所にもよりますが)いい結果が出る確率が高いです。この 引く メソッドもそうなるように調整してみましょう。 いい結果が出なかった人が、次にもう一回引きたいと思う人もいます。そんな時にもし同じ結果が出てしまうとちょっとゲンナリしますよね。そんなわがままな人のために、前回とは必ず違う結果が出る 再度引く メソッドを作ってみましょう。その時、前回と違う結果を回避するためにどんな方法があるか(例えば前回の結果を引数からもらうか、それとも 引く メソッドと同じく引数なしでどうにかするか;そして前回の結果を知った上でそれをどのように回避するかなど)も自由に考えて、それぞれのアプローチのメリットデメリットも考えてみましょう。 上記のブラッシュアップをリリースしてみましょう。その時、本記事にも既に出てた「セマンティックバージョニング」でバージョン番号を付ける際にはどのバージョンにすべきかも考えておきましょう。そしてリリースノートも忘れずに書いておきましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIRefreshControlを使ったPullToRefreshでrefreshのタイミングを変えたい

はじめに を参考にしたUIRefreshControlに addTarget(self, action: #selector(refresh), for: .valueChanged) するやり方だとrefreshのタイミングに違和感があったので別のタイミングでrefreshを行いたかった 結論 UIScrollViewのdelegateの任意のタイミングでrefreshの処理を呼び出しましょう 個人的にはscrollViewWillEndDraggingかscrollViewWillBeginDeceleratingぐらいで refreshするのがsafariやtwitterのPullToRefreshに近くてしっくりきました class XXXViewController: UIViewController, UIScrollViewDelegate { @IBOutlet weak var webView: WKWebView! { didSet { webView.scrollView.delegate = self webView.scrollView.refreshControl = UIRefreshControl() } } override func viewDidLoad() { super.viewDidLoad() } private func refresh() { reload() } private func didRefresh() { webView.scrollView.refreshControl?.endRefreshing() } // 指を離してスクロール位置が上まで戻った時 func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { guard let isRefreshing = webView.scrollView.refreshControl?.isRefreshing else { return } if isRefreshing { webView.refresh() } } // 指を離した時 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { guard let isRefreshing = webView.scrollView.refreshControl?.isRefreshing else { return } if isRefreshing { webView.refresh() } } } おまけ addTargetするやり方も一応記載しておきます 下に引っ張ると閾値(50ptぐらい?)を超えたタイミングでrefreshが呼び出されます class XXXViewController: UIViewController { @IBOutlet weak var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() webView.scrollView.refreshControl = UIRefreshControl() webView.scrollView.refreshControl?.addTarget(self, action: #selector(refresh), for: .valueChanged) } @objc private func refresh() { reload() } private func didRefresh() { webView.scrollView.refreshControl?.endRefreshing() } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む