Xcodeを開いた状態で、File -> New -> Playground でPlaygroundを開きます。
このような画面が開いたらOK
ソフトウェアエンジニアの技術ブログ:Software engineer tech blog
随机应变 ABCD: Always Be Coding and … : хороший
Xcodeを開いた状態で、File -> New -> Playground でPlaygroundを開きます。
このような画面が開いたらOK
命令文をクリックして、X-Code右上のQuick Help Inspectorを開く
なるほど、これは便利だわ
びっくりした
これだとエラーになる。
Text(bousai.population)
これでOK
Text("\(bousai.population)")
Swiftかなり難しいな
お菓子の虜 web APIの情報を使ってiOSで表示する
e.g.: https://www.sysbird.jp/webapi/?apikey=guest&keyword=%E3%82%AB%E3%83%AC%E3%83%BC%E5%91%B3&format=json
https://www.sysbird.jp/webapi/?apikey=guest&keyword=%E3%82%AB%E3%83%AC%E3%83%BC%E5%91%B3&format=json&max=10
request parameters: id, type, year, keyword, max, order
response: status, count, item(id, name, maker, price, type, regist, url, image, comment)
@StateObject, @State, @Bindingを学ぶ
検索画面はContentView.swiftで一覧表示処理、OkashiData.swiftでカスタムクラスで実装
OkashiData.swift
L StructではObservableOjectは利用できない、 classで定義する必要がある
L ObservableOjectはカスタムクラス内でデータの状態を管理するために利用
class OkashiData: ObservableObject { func searchOkashi(keyword: String) async { print(keyword) } }
Taskは一連の流れを処理する
ContentView.swift
struct ContentView: View { @StateObject var okashiDataList = OkashiData() @State var inputText = "" var body: some View { VStack { TextField("キーワード", text: $inputText, prompt: Text("キーワードを入力してください。")) .onSubmit { Task { await okashiDataList.searchOkashi(keyword: inputText) } } .submitLabel(.search) .padding() } } }
WebAPIのリクエストURLを組み立てる
OkashiData.swift
func searchOkashi(keyword: String) async { print(keyword) guard let keyword_encode = keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return } guard let req_url = URL(string: "https://www.sysbird.jp/webapi/?apikey=guest&format=json&keyword=\(keyword_encode)&max=10&order=r") else { return } print(req_url) }
レスポンスデータ(JSON)を記憶する構造体
L Itemとすることで複数の構造体を保持できる配列として保存
L ?を付与してnilを許容するオプショナル型として宣言する
struct ResultJson: Codable { struct Item: Codable { let name: String? let url: URL? let image: URL? } let item: [Item]? }
URLSessionでデータをダウンロード
URLSession.sharedで簡素に実行
do { let(data, _) = try await URLSession.shared.data(from: req_url) let decoder = JSONDecoder() let json = try decoder.decode(ResultJson.self, from: data) print(json) } catch { print("エラーが出ました") }
最近のiOSはマルチコアプロセッサが搭載されている
### 取得したデータをListで一覧表示
Itemの構造体を作成し、List表示
L Identifiableに準拠すると、一意に識別できる型として定義できる
L uuidを用いてランダムな一意の値を生成
import SwiftUI import UIKit struct OkashiItem: Identifiable { let id = UUID() let name: String let link: URL let image: URL }
@StateObject、ObservableObjectを使用すると@Publishedを使用できる
プロパティラッパーはプロパティをラップして機能を追加する
guard let items = json.item else {return} DispatchQueue.main.async { self.okashiList.removeAll() } for item in items { if let name = item.name, let link = item.url, let image = item.image { let okashi = OkashiItem(name: name, link: link, image: image) DispatchQueue.main.async { self.okashiList.append(okashi) } } } print(self.okashiList)
### リストで一覧表示
var body: some View { VStack { TextField("キーワード", text: $inputText, prompt: Text("キーワードを入力してください。")) .onSubmit { Task { await okashiDataList.searchOkashi(keyword: inputText) } } .submitLabel(.search) .padding() List(okashiDataList.okashiList) { okashi in HStack { AsyncImage(url: okashi.image) { image in image .resizable() .aspectRatio(contentMode: .fit) .frame(height: 40) } placeholder: { ProgressView() } Text(okashi.name) } } } }
### Webページの表示
SFSafariViewControllerでWebページを表示
SafariView.swift
L SafariServicesでアプリの中でsafariを起動する
import SafariServices struct SafariView: UIViewControllerRepresentable { var url: URL func makeUIViewController(context: Context) -> SFSafariViewController { return SFSafariViewController(url: url) } func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context){ } }
struct ContentView: View { @StateObject var okashiDataList = OkashiData() @State var inputText = "" @State var showSafari = false var body: some View { VStack { TextField("キーワード", text: $inputText, prompt: Text("キーワードを入力してください。")) .onSubmit { Task { await okashiDataList.searchOkashi(keyword: inputText) } } .submitLabel(.search) .padding() List(okashiDataList.okashiList) { okashi in Button(action: { showSafari.toggle() }){ HStack { AsyncImage(url: okashi.image) { image in image .resizable() .aspectRatio(contentMode: .fit) .frame(height: 40) } placeholder: { ProgressView() } Text(okashi.name) } } } } } }
これは凄い
EffectView.swift
struct EffectView: View { @Binding var isShowSheet: Bool let captureImage: UIImage @State var showImage: UIImage? @State var isShowActivity = false var body: some View { VStack { Spacer() if let unwrapShowImage = showImage { Image(uiImage: unwrapShowImage) .resizable() .aspectRatio(contentMode: .fit) } Spacer() Button(action: {}){ Text("エフェクト") .frame(maxWidth: .infinity) .frame(height: 50) .multilineTextAlignment(.center) .background(Color.blue) .foregroundColor(Color.white) } .padding() Button(action: {}){ Text("閉じる") .frame(maxWidth: .infinity) .frame(height: 50) .multilineTextAlignment(.center) .background(Color.blue) .foregroundColor(Color.white) } .padding() } .onAppear { showImage = captureImage } } } struct EffectView_Previews: PreviewProvider { static var previews: some View { EffectView( isShowSheet: Binding.constant(true), captureImage: UIImage(named: "preview_use")!) } }
CoreImageに多数の編集機能がある
なるほど、カメラ機能の実装はiPhone端末がないとダメだな
ActivityView.swift
L Anyは任意の値を表すデータ型、どんな型でもOK
import SwiftUI struct ActivityView: UIViewControllerRepresentable { let shareItems: [Any] func makeUIViewController(context: Context) -> UIActivityViewController { let controller = UIActivityViewController( activityItems: shareItems, applicationActivities: nil) return controller } func uupdateUIViewController( _ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>){ } }
ContentView.swift
L 写真がないこともある変数をオプショナル変数という アンラップ処理をする
Button(action: { if let _ = captureImage{ isShowActivity = true } }){ Text("SNSに投稿する") .frame(maxWidth: .infinity) .frame(height: 50) .multilineTextAlignment(.center) .background(Color.blue) .foregroundColor(Color.white) } .padding() .sheet(isPresented: $isShowActivity){ ActivityView(shareItems: [captureImage!]) } }
### フォトライブラリから写真を取り込めるようにする
PHPickerViewControllerはPhotoKitで提供されている機能
coordinatorを使用する
struct PHPickerView: UIViewControllerRepresentable { @Binding var isShowSheet: Bool @Binding var captureImage: UIImage? class Coordinator: NSObject, PHPickerViewControllerDelegate { var parent: PHPickerView init(parent: PHPickerView){ self.parent = parent } func picker( _ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]){ if let result = results.first { result.itemProvider.loadObject(ofClass: UIImage.self){ (image, error) in if let unwrapImage = image as? UIImage { self.parent.captureImage = unwrapImage } else { print("使用できる写真がないです") } } } else { print("選択された写真はないです") } parent.isShowSheet = false } } }
func makeCoordinator() -> Coordinator { Coordinator(parent: self) } func makeUIViewController( context:UIViewControllerRepresentableContext<PHPickerView>) -> PHPickerViewController { var configuration = PHPickerConfiguration() configuration.filter = .images configuration.selectionLimit = 1 let picker = PHPickerViewController(configuration: configuration) picker.delegate = context.coordinator return picker } func updateUIViewController( _ uiViewController: PHPickerViewController, context: UIViewControllerRepresentableContext<PHPickerView>){ }
### カメラとフォトライブラリーの選択画面
.actionSheet(isPresented: $isShowAction){ ActionSheet(title: Text("確認"), message: Text("選択してください"), buttons: [ .default(Text("カメラ"), action: { isPhotolibrary = false if UIImagePickerController.isSourceTypeAvailable(.camera){ print("カメラは利用できます") isShowSheet = true } else { print("カメラは利用できません") } }), .default(Text("フォトライブラリー"), action: { isPhotolibrary = true isShowSheet = true }), .cancel() ]) }
なるほど、カメラを使えると中々面白い
– カメラが起動し撮影できる
– SNSなどでシェアできる
カメラの起動はUIImagePickerControllerクラス、Coordinatorを使う
delegateメソッドを使って撮影後の写真を画面に表示できる
ContentView.swift, ImagePickerView.swift, ActivityView.swiftを作成する
UIKitはiOS開発に中核となるコントロール群
ImagePickerView.swiftを作成
L UIKitは自動的にimportされている
L 写真と撮影画面を閉じるフラグ設定
L UIImageは画像を管理するクラス、 Coordinator機能を利用する
import SwiftUI struct ImagePickerView: UIViewControllerRepresentable { @Binding var isShowSheet: Bool @Binding var captureImage: UIImage? }
coordinator class追加
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { let parent: ImagePickerView init(_ parent: ImagePickerView){ self.parent = parent } func imagePickerController( _ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]){ if let originalImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { parent.captureImage = originalImage } parent.isShowSheet = false } func imagePickerControllerDidCancel( _ picker: UIImagePickerController) { parent.isShowSheet = false } }
UIImagePickerController.InfoKey.originalImageでカメラで撮影した写真が取得できる
### Coordinator classとUIViewControllerRepresentable
func makeUIViewController ( context: UIViewControllerRepresentableContext<ImagePickerView>) -> UIImagePickerController { let myImagePickerController = UIImagePickerController() myImagePickerController.sourceType = .camera myImagePickerController.delegate = context.coordinator return myImagePickerController } func updateUIViewController( _ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePickerView>){ }
カメラを使用するときにはカメラの動きを指示するオプションを設定する
プロパティは sourceType, mediaType, cameraDevice, cameraFlashModeなどがある
delegateとは、あるクラスで行いたい処理の一部を他のクラスに任せたり、任せた処理を指定したクラスに通知する仕組み
protocolから処理を依頼されるクラスがある
### カメラの起動処理
カメラのプロパティを設定する行を追加
Privacy – Camera Usage Description : 写真を撮影するためにカメラを利用します。
プロパティリストはアプリの稼働に必要な設定情報を管理
### カメラ起動処理
struct ContentView: View { var body: some View { VStack { Spacer() Button(action: { if UIImagePickerController.isSourceTypeAvailable(.camera){ print("カメラは使用できます") } else { print("カメラは使用できません") } }){ Text("カメラを起動する") .frame(maxWidth: .infinity) .frame(height: 50) .multilineTextAlignment(.center) .background(Color.blue) .foregroundColor(Color.white) } } } }
### カメラを起動して撮影
@State var captureImage: UIImage? = nil @State var isShowSheet = false // 省略 Spacer() if let unwrapCaptureImage = captureImage { Image(uiImage: unwrapCaptureImage) .resizable() .aspectRatio(contentMode: .fit) } // 省略 Button(action: { if UIImagePickerController.isSourceTypeAvailable(.camera){ print("カメラは使用できます") isShowSheet = true } else { print("カメラは使用できません") } }){ Text("カメラを起動する") .frame(maxWidth: .infinity) .frame(height: 50) .multilineTextAlignment(.center) .background(Color.blue) .foregroundColor(Color.white) } // 省略 .sheet(isPresented: $isShowSheet){ ImagePickerView( isShowSheet: $isShowSheet, captureImage: $captureImage) }
カメラは覚えることが多いが、アプリで一番面白そうな分野ではある
@AppStorageはデータを永続化するUserDefaultsから値を読み込みする
UserDefaultはアプリで利用する値を保存する機能
L ここではtimer_valueというkeyにtimeValueの初期時10を導入している
@State var timerHandler : Timer? @State var count = 0 @AppStorage("timer_value") var timerValue = 10
1秒ごとに呼び出してcountを+1とし、残り0でタイマーを止める
func countDownTimer() { count += 1 if timerValue - count <= 0 { timerHandler?.invalidate() } }
タイマー開始
func startTimer() { if let unwrapedTimerHandler = timerHandler { if unwrapedTimerHandler.isValid == true { return } } if timerValue - count <= 0 { count = 0 } timerHandler = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ _ in countDownTimer() } }
### タイマーの保存
SettingView.swift
@AppStorage("timer_value") var timerValue = 10
シミュレータで確認する
### アラートの表示
.alert(isPresented: $showAlert){ Alert(title: Text("終了"), message: Text("タイマー終了時間です"), dismissButton: .default(Text("OK"))) }
すげえええええええええええ
スタート、ストップボタンとカウントダウンの秒数を表示させるテキストを配置
スタートでカウントダウンを開始再開、ストップで一時停止
秒数設定で終了時間を設定できる
画面遷移はNavigationViewを利用、カウントダウン開始秒数をpickerを配置して設定 Backでタイマー画面に戻る
ContentView.swift
L NavigationViewは先頭画面であることを宣言
L .toolbarはボタンを配置
L navigationBarTrailingで右側に配置 navigationBarLeadingは右側、.bottomBarは下部
struct ContentView: View { var body: some View { NavigationView { VStack { Text("タイマー画面") } .toolbar { ToolbarItem(placement: .navigationBarTrailing){ NavigationLink(destination: SettingView()){ Text("秒数設定") } } } } .navigationViewStyle(StackNavigationViewStyle()) } }
### タイマーの色を定義
Asset.xcassetsでcolorsetを追加、「Any Appearance」を選択
218, 78, 122にRGBを設定
Darkモードで208, 68, 112で設定
同様にストップの色も定義する
ZStack { Image("backgroundTimer") .resizable() .ignoresSafeArea() .aspectRatio(contentMode: .fill) VStack(spacing: 30.0) { Text("残り10秒") .font(.largeTitle) HStack { Button(action: {}){ Text("スタート") .font(.title) .foregroundColor(Color.white) .frame(width: 140, height: 140) .background(Color("startColor")) .clipShape(Circle()) } Button(action: {}){ Text("ストップ") .font(.title) .foregroundColor(Color.white) .frame(width: 140, height: 140) .background(Color("stopColor")) .clipShape(Circle()) } } } }
### Pickerを配置
SettingView.swift
L pickerStyle(.wheel)でPickerホイール配置
struct SettingView: View { @State var timerValue = 10 var body: some View { ZStack { Color("backgroundSetting") .ignoresSafeArea() VStack { Picker(selection: $timerValue){ Text("10") .tag(10) Text("20") .tag(20) Text("30") .tag(30) Text("40") .tag(40) Text("50") .tag(50) Text("60") .tag(60) } label: { Text("選択") } .pickerStyle(.wheel) } } } }
うむ、覚えることが広いな
ContentView.swift
struct ContentView: View { @State var inputText: String = "" @State var dispSearchKey: String = "" var body: some View { VStack { TextField("キーワード", text: $inputText, prompt: Text("キーワードを入力してください")) .onSubmit { dispSearchKey = inputText } .padding() MapView(searchKey: dispSearchKey) } } }
ContentView.swift
struct ContentView: View { @State var inputText: String = "" @State var dispSearchKey: String = "" @State var dispMapType: MKMapType = .standard var body: some View { VStack { TextField("キーワード", text: $inputText, prompt: Text("キーワードを入力してください")) .onSubmit { dispSearchKey = inputText } .padding() ZStack(alignment: .bottomTrailing){ MapView(searchKey: dispSearchKey, mapType: dispMapType) Button(action: { if dispMapType == .standard { dispMapType = .satellite } else if dispMapType == .satellite { dispMapType = .hybrid } else if dispMapType == .hybrid { dispMapType = .hybridFlyover } else if dispMapType == .hybridFlyover { dispMapType = .mutedStandard } else { dispMapType = .standard } }) { Image(systemName: "map") .resizable() .frame(width: 35.0, height: 35.0, alignment: .leading) } .padding(.trailing, 20.0) .padding(.bottom, 30.0) } } } }
うーむ、これは凄いわ