内容
- SwiftUI Tutorial の応用
開発環境
ハードウエア
項目 | |
---|---|
PC | MacBook Air(M1,2020) メモリ:16GB ストレージ:1TB |
実機 | iPhoneSE(2nd Generation 128GB iOS 15.3.1) |
PCと実機を接続する | USB-C Digital AV Multiportアダプタ |
ソフトウエア
項目 | |
---|---|
言語 | Swift 5.5.2 |
IDE | Xcode Ver 13.2.1 |
その他 | Visual Studio Code Ver 1.64.2 |
バージョン管理
アプリの内容
画面
機能
駅一覧画面


駅詳細画面
応用
画面上部にタブを追加
- LNE / MoneryForward ME 等に実装されている
駅一覧画面
import SwiftUI
struct StationListView: View {
@EnvironmentObject var modelData: ModelData
@State private var showFavoriteOnly = false
@State private var selection: Line = .Takarazuka
var filteredStations: [Station] {
modelData.stations.filter { station in
(!showFavoriteOnly || station.isFavorite) && station.line == selection
}
}
var body: some View {
// GeometryReader を使うことがポイント
GeometryReader { geometry in
NavigationView {
VStack(spacing: .zero) {
// LineTabView() で画面上部のタブを実装
LineTabView(selection: $selection, geometrySize: geometry.size)
List{
Toggle(isOn: $showFavoriteOnly){
Text("お気に入りのみ表示")
}
ForEach(filteredStations) { station in
NavigationLink {
StationDetail(station: station)
} label: {
StationRow(station: station)
}
}
}
Spacer()
}
.navigationBarTitle("駅一覧",displayMode: .inline)
}
.navigationViewStyle(.stack)
// iPad のサイドビューでは画面上部タブのレイアウトが崩れるため、.navigationViewStyle を .stack に指定している
}
}
}
struct StationListView_Previews: PreviewProvider {
static var previews: some View {
StationListView()
.environmentObject(ModelData())
}
}
画面上部のタブ
import SwiftUI
import MapKit
struct LineTabView: View {
@Binding var selection: Line
var geometrySize: CGSize
var body: some View {
HStack(spacing: .zero) {
ForEach(Line.allCases,id: \.self){ line in
VStack {
Button {
self.selection = line
print("\(line.getLineName())を選択しました")
} label: {
Text("\(line.getLineName())")
.font(.body)
.fontWeight(.heavy)
.foregroundColor(self.selection == line ? line.getLineColor() : .gray)
}
.frame(width: geometrySize.width / CGFloat(Line.allCases.count), height: 25)
Rectangle()
.fill(self.selection == line ? line.getLineColor() : .gray)
.frame(width: geometrySize.width / CGFloat(Line.allCases.count), height: 2)
}
}
}
}
}
struct LineTabView_Previews: PreviewProvider {
static var geometry: CGSize = CGSize(width: 200, height: 44)
static var previews: some View {
LineTabView(selection: .constant(.Takarazuka), geometrySize: geometry)
}
}
その他
隣の駅を表示する処理のテストコードを書く & XCode でテストを実行
import XCTest
@testable import Sample
class SampleTests: XCTestCase {
// 省略
func testGetNextStation() throws {
var count = 0
let stations = ModelData().stations
stations.map { station in
print("\(count) : \(station)")
count += 1
}
let takarazukaOsakaUmeda = stations[0]
let takarazukaNakatsu = takarazukaOsakaUmeda.getNextStation()
// 期待する結果とコードの処理結果が等しければ、SUCCESS
XCTAssertEqual(takarazukaNakatsu?.id, "takarazuka02")
let takarazukaTakarazuka = stations[18]
XCTAssertEqual(takarazukaTakarazuka.getNextStation()?.id, nil)
let kobeOsakaUmeda = stations[19]
let kobeNakatsu = kobeOsakaUmeda.getNextStation()
XCTAssertEqual(kobeNakatsu?.id, "kobe02")
let kobeKobeSannomiya = stations[34]
XCTAssertEqual(kobeKobeSannomiya.getNextStation()?.id, nil)
let kyotoOsakaUmeda = stations[35]
let kyotoJuso = kyotoOsakaUmeda.getNextStation()
XCTAssertEqual(kyotoJuso?.id, "kyoto03")
let kyotoKyotoKawaramachi = stations[62]
XCTAssertEqual(kyotoKyotoKawaramachi.getNextStation()?.id, nil)
}
func testGetPreviousStation() throws {
let stations = ModelData().stations
let takarazukaOsakaUmeda = stations[0]
XCTAssertEqual(takarazukaOsakaUmeda.getPreviousStation()?.id, nil)
let takarazukaNakatsu = stations[1]
XCTAssertEqual(takarazukaNakatsu.getPreviousStation()?.id, takarazukaOsakaUmeda.id)
let takarazukaTakarazuka = stations[18]
XCTAssertEqual(takarazukaTakarazuka.getPreviousStation()?.id, "takarazuka55")
}
// 省略
}
テスト実行結果
Test Successed
今後
- おでかけスポット画面を追加
- GitHub Actions を試してみる
感想
- 慣れたら UIKit よりもラク
- ダークモードが自動で反映されるのでラク
- Enum を上手く使うと改修がラク
参考資料
SwiftUI Tutorial
画面上部のタブを実装する際に参考にした記事