Swiftで遊ぼう! - 1020 - 番外編 2010年モデルのVAIO Zが復活
SwiftUIプログラミングの勉強は正直滞っていて、ちょっと焦っている。先月誕生日を迎え、55歳になってしまったのでタイムリミットは後5年。本当に焦るな。
なんて、考えているときに、古いVAIO Zが部屋の片隅に埋もれていたのを発見。10年前のモデルだが、VAIO Zのフラグシップだったので、プロセッサはCore i7。メモリも8GB載っていて、ディスクはSDDで256GB。13インチスクリーンでありながら重量は1.4kg、MacBook Pro 15inchと比較すると軽く感じる。どういう訳か使わなくなり埋もれてしまっていた。使わなくなった理由は、いくつかあるけど、10年前と比較すると、Windowsがないとできないことが減り、Macだけで事足りようになったからだ。
久しぶりに電源を入れると普通に起動してWindows 7が立ち上がった。でも、なんというタイミングの悪さだろう。Windows 7のサポートが今年の1月に終了したところで、このまま利用するのはセキュリティ上の不安がある。それでもWindows Updateが普通にアップデイトを促してきたので、「あれ?まだアップデイトできるんだ。」なんて気楽に考えてアップデイトさせていたら、最初の起動画面に、もうこのバージョンのWindowsはサポートされていません、というようなブルー画面が出てくるようになった。この画面をみると、やはり不安になる。オフラインの環境でテキストエディタとして使うなら問題ないんじゃないかと考えてATOK Passportをインストールしようと思ったら、サポートされていません、という冷たいお言葉。
やはりWindowsマシーンとして使うのに無理があるので、この古いマシーンをどうしようかと悩んで、思いついたのがLinuxマシーン化だ。ほとんど何も考えず選んだディストリビューションは「Ubuntu」だ。どうせ使えないマシーンなんで、「やっちゃえ」なんて感じで、インストールしました。今日インストールしたところですが、バージョンは、安定バージョンの「Ubuntu 20.04.1 LTS」で、指示通りにインストール作業をしたら、何も問題なく終了できた。当然のように躊躇なくWindows 7を削除して後戻りできないようにしてしまった。それでも問題なく起動したときにはちょっと感動。デフォルトで日本語入力ができなかったけど、調べてみると、IBusで「Mozc」をインストールできて日本語語入力も問題なし。「Meiryo」フォントも組み込んでみると。あら不思議、10年前のノートブックが最新モデルのようにキビキビ動いて見た目もきれい。Google Driveを使うと、Linux、Windows 10、MacOSXでファイル共有ができマルチプラットフォームで作業ができるようになったんで、これは使える!とちょっと嬉しくなったが...
問題がないわけではない。古いプロセッサは消費電力が多いうえに、メーカーが最適化したOSを使っているわけではないので、ファンが回りっぱなし。これじゃバッテリーが持たないですね。10年前の機種なのでバッテリーもヘタっていると思われるが、ACアダプター無しで3時間弱しか持たない。これで5,6時間持つと使えるマシーンになるけど、しばらく文章書きに使用してみようと思っている。日本語変換のMozcもネットで言われているほど変換効率が悪いわけではない。ここの文章をMozcを使って書いているけど、こういう普通の文章なら問題ないだろう。ATOKが持っている文章入力支援のための辞書が無いのは不便だけど、 語彙力が高ければ問題ないし、ネットにアクセスできる環境ならあまり問題にならないかもしれない。
ということで、この古いVAIO Zがいきなり文章書きのメインマシーンになってしまいそうだ。
Swiftで遊ぼう! - 1019 - EnvironmentObject実装で疑問❓
2021年2月15日追記:AppDelegateもSceneDelegateも使わない「SwiftUI App」スタイルの実装法の説明は:Swiftで遊ぼう! - 1023 - EnvironmentObjectの取り扱い - Swiftで遊ぼう! on Hatena
SwiftUIプログラミングで重要な設計パターンはMVVMである。Model、View、そしてViewModelの3つのパターンを考えてアプリを開発していくやり方だ。Swift言語が世の中に出てきた頃のiOS開発の中心はMVCだった。しかし、オブジェクト指向のクラス中心の開発から、プロトコール指向で構造体を中心に扱うプログラミング開発に時代がシフトしていくなか、SwiftUIを使ったMVVM設計が重要になってきている。
実は、このSwiftUIを使ったMVVM設計、やみくもに重要と言いたいわけではなく、プログラミング初心者にとって重要、と言うべきかもしれない。概念が理解しやすいからだ。たぶん、ベテランプログラマーは、開発環境が未熟なSwiftUI環境に100%シフトすることはできないだろう。それでも、従来のリファレンス型のクラス設計の中、メモリの参照関係を意識しながら、ユーザーインターフェイズをAutoLayoutを使って考えながらコードするのは、初心者にとって難しい。また、効率よくプログラミングするために色々あるデザインパターンも意識しながら設計しないといけないので、頭がこんがらがって挫折した初心者も多いだろう。私も悪戦苦闘を続けていたところで、SwiftUIが舞い降りてきた。まさに救世主が現れた感じだった。
プログラミング初心者に優しいといいながら、全くの初心者には、やはり概念的に難しいところもある。どの分野にしろ、過去の知識を土台にして、新しい知識が積み上がっているから仕方ない。オブジェクト指向プログラミング開発を勉強したから、プロトコール指向の構造体プログラミング開発が楽に感じるだけ。MVCを知っているからMVVMがシンプルに感じるだけかもしれない。
MVVMを使って開発する時に重要な概念が1つある。Viewの状態を監視するために使うプロパティ・ラッパーだ。概念的な説明は省く、というか私が人に説明できるほどちゃんと理解していないからできない、と言う方が正しい。wrappedValueとprojectedValueを包み隠して利用する型を宣言して使うって感じで理解しているぐらいだ。まあ、重要なことはいかに利用するかだ。数学の公式でも理解してなくても利用して問題が解ければいいんだ。全ての概念を理解する必要はない。理解しなければならないのは、コンピュータサイエンスを専攻している人たちだろう。そう考えてプログラミングに取り組む方が気が楽なんで、何でも完全に理解して取り組もうとしない方がいいんじゃないかと思っている。
そうは言うもののある程度理解して取り組まないと、エラーが発生した時に全く自分で解決できないことになる。ある程度自分で解決できるようになっているが分からないことも多い。ここまで読んでくださったベテランプログラマーの人にお訊ねしたいことがあります。助言していただけないでしょうか?
まず、Navigationを使ったアプリで、ContentViewから、リスト化されたボタンを押すことで、内容の異なるDetailViewに遷移する単純なアプリを設計しました。同じDetail Viewを使って、内容だけ変更したいので、内容を保持するModel構造体を用意するのですが、表示をコントロールするためにViewModelも組み込むので、次のようなObservableObjectクラスを用意した。
import Foundation // Model struct Person { var name: String var age: Int } // ViewModel struct PersonViewModel { var person: Person var description: String { return person.name + " " + String(person.age) + "歳" } } class AppData: ObservableObject { @Published var userData: [PersonViewModel] init() { userData = [ PersonViewModel(person: Person(name: "Taro", age: 5)), PersonViewModel(person: Person(name: "Hana", age: 7)), PersonViewModel(person: Person(name: "Chie", age: 12)) ] } }
当然のようにViewとViewModelのようなユーザーインターフェイスを含まないコードをする場合、SwiftUIフレームワークを組み込まないようにする。本来なら、保存したデータベースやインターネットから初期値をダウンロードする設計が現実的ではあるが、これはサンプルコードなんで初期化ステップで固定されたデータを組み込むようにした。
これをEnvironmentObjectとして読み込むのが次のDetailViewである。それを次のように実装する。
import SwiftUI struct DetailView: View { @EnvironmentObject var appData: AppData var index: Int var body: some View { VStack { Text(appData.userData[index].description) Spacer() }.padding() .navigationBarTitle(Text("Detail info")) } } struct DetailView_Previews: PreviewProvider { static var previews: some View { NavigationView { DetailView(index: 1).environmentObject(AppData()) } } }
ここで、「@EnvironmentObject var appData: AppData」と宣言だけしている。ここで、この構造体の中でインスタンス化はしていない。そりゃ当然ですよね。この構造体の中でインスタンス化すれば、この構造体のスコープ内でしか使えないからだ。EnvironmantObjectとして宣言しているということは、同じアプリ内のView全体で共通して使用するという意味なので、どこでインスタンス化すべきか考えないといけないんです。
まさに、それが私の疑問である。
まず、その問題提起をする前に、ContentViewのコードもここに紹介する。
import SwiftUI struct ContentView: View { @State var selected: Int? = nil var body: some View { NavigationView { VStack(spacing: 15) { NavigationLink("Person 1", destination: DetailView(index: 0), tag: 0, selection: $selected) NavigationLink("Person 2", destination: DetailView(index: 1), tag: 1, selection: $selected) NavigationLink("Person 3", destination: DetailView(index: 2), tag: 2, selection: $selected) Spacer() }.padding() .navigationBarTitle("Main Menu") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { NavigationView { ContentView() } } }
この3つのファイルを用意して、プレビューを確認するとちゃんとプレビューはみることができる。そりゃ当然。でも、実際にシミュレーターを立ち上げようとするとエラーが出現する。それも当然。AppDataがインスタンス化されていないからだ。
じゃあ、どこでインスタンス化すればいいのか?
ここが今日の疑問である
まず1つ目の実装法は次のようにAppDelegateで次のようにコーディング。
import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var myData: AppData! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. myData = AppData() return true } // 後に続く ...
そして、SceneDelegate内のsceneメソッドに、AooDelegateでインスタンス化したインスタンスを組み込む。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Create the SwiftUI view that provides the window contents. let delegate = UIApplication.shared.delegate as! AppDelegate let contentView = ContentView().environmentObject(delegate.myData) // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } }
これも納得のいく実装法だと思っている。でも、次のようにSceneDelegate内だけでインスタンス化して組み込みも完了するやり方もあるんですよね。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Create the SwiftUI view that provides the window contents. let myData = AppData() let contentView = ContentView() // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView.environmentObject(myData)) self.window = window window.makeKeyAndVisible() } }
どちらでもちゃんと動くんです。でも、どちらの実装法が理想的なのか、私には分からないんだ。ベテランプログラマーの皆さん、どちらの実装法がいいのか教えてください。理由もあれば、些細なことでもいいのでアドバイスをお願いします。
よろしくお願い申し上げます。いつまで経って素人から脱却できないド素人オヤジプログラマーです。はやくアプリを販売できる人間になりたい!
Swiftで遊ぼう! - 1018 - Combineフレームワークのオペレーターを復習
皆さん、新しいXcode12のβ版が利用できるんで、試している人も多いのではないでしょうか?WWDC2020の動画配信も見て勉強していることでしょう。
私は、全く新しい内容に取り組んでいないんです。チンタラとSwiftUIを勉強してます。
更新を長い間しないと、もうプログラミングの勉強をしていないと思われるかもしれないんでちょっと更新。Combineフレームワークで必要なオペレーターのリストを備忘録として残しておきます。全てのオペレーターの挙動をちゃんと理解していないといけないのですが、まだまだ完全に理解できてませんね。
- Map(upstream: Publisher, transform: Closure)
- Filter(upstream: Publisher, isIncluded: Closure)
- Debounce(upstream: Publisher, dueTime: SchedulerTimeIntervalConvertible, scheduler: Queue)
- RemoveDuplicates(upstream: Publisher, predicate: Closure)
- ReceiveOn(upstream: Publisher, scheduler: Closure)
- TryMap(upstream: Publisher, transform: Closure)
- FlatMap(upstream: Publisher, maxPublisher: Demand, transform: Closure)
- Catch(upstream: Publisher, handler: Closure)
- CombineLatest(Publisher, Publisher)
- Merge(Publisher, Publisher)
- Zip(Publisher, Publisher)
- Decode(upstream: Publisher, decoder: Coder)
- Autoconnect(upstream: Upstream)
やっとパブリシャー&サブスクライバーの理解が深くなりました。画面のカウンターが自動的にカウントアップしていくコードが次のものです
import SwiftUI class ViewData: ObservableObject { @Published var counter: Int = 0 let timerPublisher = Timer.publish(every: 1, on: RunLoop.main, in: .default) .autoconnect() } struct ContentView: View { @ObservedObject var viewData = ViewData() var body: some View { Text("カウント: \(viewData.counter)") .onReceive(viewData.timerPublisher, perform: { value in self.viewData.counter += 1 }) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
Swiftで遊ぼう! - 1017 - Xcodeにコメントを加える
最初っから難しいプロジェクトに取り組むことを諦めて非常に単純なプロジェクトに取り組んでいる。自分の子供向けの学習プログラムを作ってみようと思って、取り組み始めたわけ。
でも、コードを書いていても、やっぱりオヤジは、自分で書いたコードても後で見返すと分からなくなってしまう。Xcodeでコーディングしていても、詳しいコメントを残していかないと、後で振り返っても覚えてないことばかりになる。ということで、コメントの書き方は非常に重要なんだろう。書いたメソッドの説明をちゃんと記述して残していないと全く後で使えないってことになる。情けないことにXcodeにコメントを残すルールも知らなかった。
/** 角度を与えることで円周上の位置をCGPointで返すメソッド CGRectの長方形を与えて、その長方形の短辺から円を作り、そこにラジアン角度を与えることで、円周上の位置情報をCGPointで返す - important: 角度はラジアン角度を与える必要があるので、全外周長は2πである - returns: 外周の位置情報をCGPointとして返す - parameter radianAngle: 位置を得るために角度をラジアン角度として与える - parameters: - rect: 長方形を与えることでその短辺から円の半径をもとめる */ private func topPosition(for radianAngle: CGFloat, in rect: CGRect) -> CGPoint { - -
メソッドの記述直前に、こういう風にコメントを書いて、メソッドを 「⌥ (Option) + クリック」してやると次のように説明用のウインドウが立ち上がる。
最初の行に書いた文章はSummaryに表示され、1行空欄を開けるとDiscussionに表示されるみたい。例に書いているようなタグを書いて記述すれば、それぞれの説明文が表示される。何年もXcodeを扱っていて、コメントの書き方も知らなかったとは。
Swiftで遊ぼう! - 1016 - カスタムシェイプ
2021年2月23日:SwiftUIを使ってカスタムシェイプを扱う場合、SwiftUIの作法で書き換えた方がいいので、この記事のコードを修正しました。
時間があるときに少しずつ勉強しているので、忘れることも多い。でも、地道にやっていると少しずつではあるが確実に知識はついていっていると思うよ。
今日はSwiftUIで、カスタムシェイプの勉強をしてました。
SwiftUIでも、UIKitやSpriteKitでできることが基本的にできるので、カスタムシェープの扱いを勉強してました、どうやって円形を作るか基本的な知識の勉強です。
struct Circle: View { var rect: CGRect var body: some View { Path { path in path.addArc( center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width/2.0, startAngle: .degrees(0), endAngle: Angle(radians: 2.0 * Double.pi), clockwise: true) path.closeSubpath() } .fill(Color.black) } }
これを描画するためにradiansの因数、staratAngleやendAngleに変数を渡さなければならないんですが、そこでAngle型の変数が必要なんですよが、いくつかバリエーションがあるんです:
// Angleの型のパラメーターを指定して与える方法に // 次のように直接パラメーターを指定する方法 .degree(30) // そして角度を与えるパラメーターとして型から与える方法 Angle(degree: 30) // 更に ラディアスを因数として与えると次のようになる Angle(radians: 2 * Double.pi / 2 ))
そうなんです。どれも同じ結果になるんですよね。プログラミングのスタイルは1つじゃないってこと。こういうことってプログラマーにっとって基本的な知識なんですが、素人にとって混乱するところ。人生に1つの答えは無いとの同じようにプログラミングにも1つの答えは1つじゃないってこと。色々考えながら進んで行きましょう。
Swiftで遊ぼう! - 1015 - URLSessionDataTaskPublisherの基礎
URLSessionの基本をまとめたのが5年前...
5年経過したので、NSURLは消えてしまったようですが、URLSessionはネットからデータを取り込むために今でも利用されている。しかし、新しいフレームワークであるCombineと組み合わせて利用するのが一般的になってきている。Combineは「関数型プログラミング」を実現するためのフレームワークであり、SwiftUIと組み合わせることで、最も新しいiOS開発環境ということになるだろう。そこで、今回、Combineに関して少々勉強できたので、5年前のデモを新しく書き直してみた。
当初は、上記のリンクの記事を更新するつもりでしたが、SwiftUIとCombineを使って開発をすると、Autolayoutは使わないし、非同期処理も違うので、古い記事はそのままにして、新しい記事を書いてみた。
新しいプロジェクトを作ります。「Simple View Application」を選び、「User Interface」を必ず「SwiftUI」にする。そして、プロジェクト名は適当につけますが、私は「URLSessionDataTaskPublisherDemo」にしました。
SwiftUIの基本的な扱いに関して説明はしません。分からない人はSwiftUIのチュートリアルをこなしてください。SwiftUIの特徴は、MVC開発からMVVM開発への変化でないかと思ってるんですが、まだ私も勉強中で分からないことだらけ。ContentViewでユーザーインターフェイスの開発に集中。そして、Modelで処理させて、Combineなどの通知機能を利用することで自動的にViewと連携させてやるという考えじゃないだろうか。今まで使用していたUIViewContorollerも使いません。私の嫌いなAutolayoutも必要無いので、処理をどうすればいいか開発に集中できるようになっているので、これからiOS環境を勉強し始める人が羨ましいですね。
さて、画面上のボタンを押して、Amazonのサイトから画像をダウンロードさせるだけですが、その処理をContentViewには加えません。新しいModelになるファイルを作りますメニューから「File」→「New」→「File...」を選択します。そして「Swift File」を選択してから名前を「ImageLoader」にしてプロジェクト内に加えます。「import Foundation」しかないので、次のように「SwiftUI」と「Combine」を加えてください
import Foundation import SwiftUI import Combine
さて次に、アプリを立ち上げた時に表示するイメージを用意します。アプリのテスト環境は「iPhone 11」にします。すると、プロジェクトで使用するイメージのサイズは2種類でいいんです*1。先ず、プロジェクト・ナビゲータでAssets Xcassetsを選択して、下部にある「+」ボタンを選び、「New Folder」を選択します。フォルダーの名前を「Placeholder」に変更してから「+」ボタンを再び選択して「New Image Set」を選ぶと「1X」「2X」「3X」の空のスロットができます。
次に何でもいいので「png」イメージを用意します。オブジェクトのサイズを「240×240ピクセル」にしているので、1Xはそのまま、2Xは「480×480ピクセル」、そして3Xは「720×720ピクセル」の画像を用意するのですが、ここでは2Xスロットだけセットします。そして、名前を「backgroundImage」に変更します。
これで画像は用意できたので、ImageLoaderに初期値として、先ほど用意したbackgroundImageを持つUIImageを持たせるためにコードを次のように書きます。
import Foundation import SwiftUI import Combine class ImageLoader: ObservableObject { @Published var image = UIImage(named: "backgroudImage") }
ここで、2つの重要なキーワードが出てきました。ObservableObjectと@Publishedキーワードです。実はこのObservableObjectですが、Combineに含まれるんです。でも、Combineフレームワークをimportしなくても、SwiftUIのみで使えるため、最初に私は混乱しました。SwiftUIを使って、システム標準の通知機能を使うだけならCombineフレームワークは必要ないのです。外部との通信や、通知機能を自作するためにCombineが用意されています。また、非同期処理もCombineでさせる方が処理させやすいということですが、まだまだ私の知識で使いこなせるとは思ってません。
ここで用意したimgaeをContentViewで表示させてみます。次のコードをContentViewに加えます。
struct ContentView: View { @ObservedObject var loader = ImageLoader() var body: some View { VStack { Image(uiImage: loader.image!) .resizable() .frame(width: 240, height: 240) .scaledToFit() } } }
そしてランしてみましょう。当然プレビューをみてもかまいません。次のようになっているでしょう。
次にAmazonから画像データをダウンロードさせる関数をModelに用意させるのです。ここでCombineフレームワークを使います。Combineフレームワークを利用するために、Publisher、Subscriber、そしてOperatorを利用しなけらばなりません。自分で作るクラスなら、それぞれのProtocolやExtensionに準拠させないといけないのですが、URLSessionにはCombineが利用できるようになっています。URLSessionで用意されているPublisherがDataTaskPublisherなんです。このDataTaskPublisherを使ってSubsriberを作り、情報処理をOperatorでするっていう流れです。従って、DataTaskPublisherを利用すると、必ずData、URLResponnse、URLErrorの処理をしなければならないので、利用するしないに関係なく対応をしなければなりません。ということで、ImageLoaderクラスに次の変数を持たせます。
class ImageLoader: ObservableObject { @Published var image = UIImage(named: "backgroundImage") var cancellable: Cancellable? = nil var data: Data? = nil var response: URLResponse? = nil var error: URLError? = nil } }
cancellableがSubscriberですが、Pubulisherの宣言がないですよね。Publisherはシングルトン利用するので、インスタンスは作らないことにします。
次の関数を加えます。
func fetchImage() { let url = URL(string: "https://s3.amazonaws.com/ CoverProject/album/album_david_bowie_lets_dance.png") cancellable = URLSession.shared.dataTaskPublisher(for: url!) .print("subscriber now") .eraseToAnyPublisher() .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): fatalError(error.localizedDescription) } },receiveValue: { let image = UIImage(data: $0.data)! self.image = image }) }
これで自分が持っているイメージがダウンロードされたイメージと変わります。では、ContentViewにボタンを加えましょう。
struct ContentView: View { @ObservedObject var loader = ImageLoader() var body: some View { VStack { Image(uiImage: loader.image!) .resizable() .frame(width: 240, height: 240) .scaledToFit() Button(action: loader.ftechImage, label: { Text("Get Image") }) } } }
じゃあランしてボタンを押してみるといいでしょう。次のようにデビッド・ボウイのジャケットが今も変わらずダウンロードできますよ。
実はImageLoaderクラスにUIResponseやUIErrorのプロパティを持たせましたが、$0のデータしか使っていないので、流れてきたデータを殆ど捨ててます。そして、このときのデータの流れがコンソールに出るのですが、色々な情報が紛れていて興味深かったです。まだまだCombineの勉強が必要なので、もう少し勉強します。
*1:理由がわからなければSwiftで遊ぼう! - 249 - iOSの座標システム: 2020年3月 - Swiftで遊ぼう! on Hatenaで説明しているScaling値を確かめましょう。
Swiftで遊ぼう! - 1014 - Viewのライフサイクル
ふふふふふ、私のブログをお気に入りにしている皆さん、私、屋台ブルーが、iOS開発を完全に諦めてしまって、ここを放置していたと思うでしょう(その通りです)。
いやあ、皆さんの思っていたように、移り変わりの激しい開発環境についていけず、完全に放置状態になっていたのは事実です。本業の勉強も忙しく、年取って結婚したため、子どもの教育にも時間をとられ、プログラミングは二の次になっています。それでも、OOPやPOPなど、プログラミングの基本概念をある程度理解できていたので、SwiftUIが導入され、それを理解するための本を読んでもちゃんと理解できます。
- 作者:金田 浩明
- 発売日: 2019/12/21
- メディア: 単行本
この本は、Swift開発の初心者には非常に難しい内容だと思います。まだ完全に理解しているわけではありませんが、内容が理解できるということは、私も全くの初心者ということではないでしょう。
SwiftUIでの開発を勉強していますが、自分の勉強にあわせて古い記事を書き直しています。次の記事を書き直しました。
さあ、どこまで復習ができることか。