命令文をクリックして、X-Code右上のQuick Help Inspectorを開く

なるほど、これは便利だわ
びっくりした
ソフトウェアエンジニアの技術ブログ:Software engineer tech blog
随机应变 ABCD: Always Be Coding and … : хороший
命令文をクリックして、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)
}
}
}
}
うーむ、これは凄いわ
検索窓(TextField)、マップを表示するView(MapKit)を設置する
キーワードから緯度経度を検索 -> 緯度経度からピンの画面パーツ生成 -> ピンの画面パーツを地図画面に貼り付け
MapKitにはUIViewRepresentableを使用する
TextFieldでの文字入力と入力完了後(onCommit)の使い方を学ぶ
ContentView.swiftとMapView.swiftを作成する
SwiftUIのMapView.swiftを作成
アプリでMapを表示するにはMapKitをインポートする
MapKitは地図、衛星画像、ピン配置、住所から座標検索ができる
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView()
}
func updateUIView(_ uiView: MKMapView, context: Context){
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}

UIViewRepresentableを記載すると、makeUIView(viewの作成)とupdateUIView(viewの変更、再描画)が必要になる
UIViewRepresentableは、MKMapViewを使うためのラッパー
### 検索キーワードの設定
MapView.swift
struct MapView: UIViewRepresentable {
let searchKey: String
func makeUIView(context: Context) -> MKMapView {
MKMapView()
}
func updateUIView(_ uiView: MKMapView, context: Context){
print(searchKey)
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView(searchKey: "東京タワー")
}
}
ContentView.swift
struct ContentView: View {
var body: some View {
VStack {
MapView(searchKey: "東京タワー")
}
}
}
-> シュミレーターを立ち上げると”東京タワー”と表示される

シミュレータでプログラムが実行される順番
MyMapApp.swift -> ContentView.swift -> MapView.swift
### 検索キーワードから緯度経度を検索
func updateUIView(_ uiView: MKMapView, context: Context){
print(searchKey)
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(
searchKey ,
completionHandler: { (placemarks, error) in
if let unwrapPlacemarks = placemarks,
let firstPlacemark = unwrapPlacemarks.first ,
let location = firstPlacemark.location {
let targetCoordinate = location.coordinate
print(targetCoordinate)
}
})
}
追加
let pin = MKPointAnnotation()
pin.coordinate = targetCoordinate
pin.title = searchKey
uiView.region = MKCoordinateRegion(
center: targetCoordinate,
latitudinalMeters: 500.0,
longitudinalMeters: 500.0)
MKPointAnnotationはピンを置くための機能

地図の扱いは特殊ですね