普段Androidアプリ開発をしていて、この度iOSアプリ開発に手を出してみた。
その際にAndroidにおけるあれってiOSでどうやってやるの?となったことをまとめておくメモ。
随時加筆予定。
環境・技術スタック
Android…Viewベースアプリ(jetpackComposeではなく)
iOS…SwiftUI (UI,ライフサイクルともに)
Xcode…Version 12.5 (12E262)
View
(Android View) → (SwiftUI)
- FrameLayout→ZStack
- LinearLayout (横方向)→HStack
- LinearLayout (縦方向)→VStack
- ImageView→Image
- TextView→Text
- ScrollView→ScrollView…ScrollView(.vertical)
- HorizontalScrollView→ScrollView…ScrollView(.horizontal)
- SwipeRefreshLayout→無い。iOSにもUIkit時代には
UIRefreshControl
という同じようなものがあったらしいけど、SwiftUIにはないので自分で作るかUIRefreshControlをSwiftUIで使えるようにUIViewRepresentable
でラップしてやるしか無いらしい。クルクル表示だけはProgressView
というのがある。 → 自作して記事にしてみたが、長すぎたので別記事にしました。
画面遷移
Androidではタッチすると次の画面にいきたいViewにClickListnerをセットして、その中で新しいActivityのstartやFragmentのreplaceを行ったりするのが一般的だと思います。
SwiftUIでは
- まず入れ替え元にしたいViewをNavigationViewで括る
- 入れ替え先にしたいViewや、そのタイミングはNavigationViewの中においたNavigationLinkで設定する
NavigetionLinkの使い方
NavigationLink(destination : <遷移先のView>,
isActive : <@Stateをつけたboolean変数 または @EnvironmentObjectをつけたclass型の変数の中で定義している@Publishedをつけたboolean変数> ※変数名の前に$をつける。){
何も書かない or EmptyView or タッチした時に遷移が始まるようにしたいView
}
遷移タイミング
- NavigationLinkの第3引数になっているラムダ関数の中で、Viewを設定しておくとそれをタッチした時。(何もしなくてもタッチアニメーションもついている。)
- ラムダ関数の中で何もViewを設定しない or EmptyViewを設定しておくと、NavigationLink自体は不可視になるようでタッチできない。代わりに第2引数になっている変数isAliveがtrueになった時に遷移が起きる。
なので、NavigationLinkの外の他のViewにonTapGestureを設定しておいてその中でisAliveに設定してある変数をtrueにするなどする。isAlive変数は常に監視されていて、能動的にNavigationLinkに教えてあげるなどする必要はないようだ。便利! - ラムダ関数の中でViewを設定&外のViewにonTapGestureを設定を同時にしてみたところ、外のViewタップでは遷移しなくなった。どうやら両方設定されている場合はラムダ関数の方の方が強くて遷移タイミングを司るのはそちらのみになるようだ。
動的にViewのvisibilityを変える
NavigationLinkに@Stateや@Publishedな変数をisAliveとして渡すと値が変わるときに発火したけど、それと同じようにif(<@Stateな変数>)などとしておくと@Stateな変数の値が変わるたびに再評価されるようだ。なのでその中でViewを作るようにして、visibleにしたいときに変数の値を変える。
画像のサイズを調整
Imageのサイズを調整するときは、最初に.resizable()を呼ぶ。じゃないと表示は変わるけど意図しない感じ(縮尺そのまま画像の一部が指定された大きさに切り取られている?)になった。
Image("pyoko_kagamimochi")
.resizable()
.frame(
width: 380,
height: 400)
背景をつける
.background()。渡すものはViewならなんでもよさそう。Colorクラスのものを渡すと色、Imageクラスのものを渡すと画像の背景になった。
VStack{
(中略)
}.frame(
width: 380,
height: 400
)
.background(Color.red)
枠線をつける
.border(Color.blue,width: 2) // 青色の太さ2の枠線
角丸の枠線をつける
角丸の枠線はborderメソッドではひけない。
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.black, lineWidth: 2)
) // 太さ2、黒、角20の枠線
Viewの角を丸める
.cornerRadius(20)
ちなみに、.border(Color.blue,width: 2).cornerRadius(20)
のように、角丸Viewに普通の枠線を設定すると枠線の角が切れて残念な感じになる。
marginをつける
Viewには、padding()
というメソッドはあるけど、marginメソッドがない。
→実は各メソッドを呼ぶ順番は可換ではなく、呼ぶ順番に大きな意味があるようだ。
frame()
メソッドより先にpadding()
を呼ぶとpaddingを含めてひとつのframeになる=所謂padding、frame()
メソッドより後に呼ぶとView frameの外にpadding=所謂margiinになる模様。
.frame(maxWidth: .infinity,
alignment: .top)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.black, lineWidth: 2)
)
.cornerRadius(20)
.padding() // frameより後にpaddingをつけると、marginになる
Viewの幅をwrap_contentにする
frame()
メソッドに、widthやheightに関する部分を何も書かなければwrap_contentになるようだ。
Viewの幅をmatch_parentにする
frame()
メソッドに、maxWidth: .infinity
やmaxHeight: .infinity
とすると、match_parentのような動きになる。(というより実際のロジックは可能な限り大きくなるが近い?)
.frame(maxWidth: .infinity, // 横幅match_parent
alignment: .top)
Viewが表示されたらコールバックが取りたい(onViewCreated的な)
Viewに対してonAppear()
VStack{
}
.onAppear(){
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// 一秒後の遅延処理
}
}
Adds an action to perform when this view appears.
だそう。(Appleの公式ドキュメントより)
スワイプして、くるくる更新中インディケーターを出してデータ更新。更新完了し次第インディケーターを消す。
SwiftUIでは、くるくる更新中インディケーター単体の用意はある('ProgressView')が、「スワイプされたら出す」とか、「データ更新完了したタイミングで消す」などの機能は用意されていない。
UIKit時代のViewのUIRefreshControl
をSwiftUIで使えるようにUIViewRepresentable
でラップしてやるか、自作するしか無い。
自作して記事にしてみたが、長すぎたので別記事にしました。
ローカルにデータを保存しておきたい
android SDKでは、SQLiteが使えるようになっているので、そちらを使っていることと思います。
それのiOSでの代替。
- CoreData : どうもiOSではこちらが主流のよう。正確にはDBではなくO/Rマッピングフレームワークらしいですが調べると結構DBの一種みたいな説明のされ方をしていたり。
- ReamやSQLも使えるみたいです。(手を出していないので真偽は不明)
また、もっと小さい簡単なKey-Valueの保存にはSharedPreferencesを使用していると思いますがその代用は、
- UserDefaults
- Keychain
などがあるらしい。ただし、UserDefaultは暗号化がしておらず他のアプリから見える可能性がある。Keychainはアプリをアンインストールしても自動で削除されないなど各々特徴があるようです。
以上はこちらの記事がわかりやすかったです。
そもそもCoreDataとは&使い方
こちらのシリーズがとても参考になりました。
CoreDataにString配列を格納するには
CoreDataを使うと、データを何型でいれるかを.cxdatamodelIdファイルで定義することになります。
SQLiteなどSQLでは例えばサーバーレスポンスの一部のKey-Valueが配列の時でもそのまま入れることはできなくて、別テーブルを作成することになりますがCoreDataではそのまま格納できます。が、実際にStringの配列を入れようとして、とても困ったのでメモ。
-
.xcdatamodelIdファイルでENTITIESのAttributeのTypeに選ぶのは
Transformable
。Transformable
で指定してあげると、バイト配列であるNSDataとしてデータを保存する動きになるので任意の型のデータを扱えるのだとか。 -
取り出した時は(nilチェック後に)
as! [String]
で強制キャストしてあげる。
例)
MemosがCoreDataのEntity名&Class名。
memoContent がTypeがTransformableのAttribute名。
struct TopView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Memos.memoTitle, ascending: true)],
animation: .default)
private var items: FetchedResults<Memos>
var body: some View {
var _memos = Array<Memo>()
self.items.forEach{ memo in
(略)
guard let c = memo.memoContent else{
return
}
var contents = Array<String>()
(c as! [String]).forEach{
contents.append($0)
}
(略)
}
return VStack{
(略)
}
}
}
- 格納する時は[String]型のインスタンスを
as NSObject
でキャストしてあげる。
例)
let newItem = Memos(context: viewContext)
(略)
var cs : [String]=[]
m.content.forEach{ c in
cs.append(c)
}
newItem.memoContent = cs as NSObject
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
※ただ、これで現状実行時エラーも出ずにちゃんと動いているように見える。
が、本当に絶対大丈夫なのか、とか絶対に無駄がないのか、とか問われると正直SwiftやObjective-Cに関する勉強が不十分であまり自信がない。ので間違っていたらご指摘いただけると嬉しいです。
※これを学ぶ過程で知ったSwiftやObjective-Cの言語に関する知識のメモ
- NSObjectとはObjective-Cにおけるほとんどのクラスの親クラス。Swiftとの関係ははっきりとしたことはわからなかった。Appleの公式のNSObjectの説明にもSwiftに関する記述はなし。(https://developer.apple.com/documentation/objectivec/nsobject) けどString配列をNSObjectにキャストできるのだからSwiftでも親クラスになっている?
- SwiftのArrayと[String]は全く同じものらしい。変数の定義や初期化でもどちらを使ってもいいようだ。