この記事の内容
SwiftUI と MVVM を題材に、メンテナンス・機能を拡張しやすいコードの書き方を考える
※ あくまで個人のメモ書き程度と捉えてください
開発するアプリ
機能
- とある鉄道会社のとある路線の駅を一覧で表示する
- 駅番号・駅名(日本語)・駅名(英語)を表示する
GitHub
画面数
- 1つのみ
ファイル
ファイル名 | 内容 | 備考 |
---|---|---|
Station_Kobe.json | 駅に関する情報を記録 | |
JSONLoader.swift | JSON ファイルからデータを取得・解析 | SwiftUI チュートリアルで JSON ファイルからデータを取得・解析するコードを流用した |
Station.swift | データモデル(駅)を定義 | |
Line.swift | 路線に関する情報を列挙型で定義 | 神戸線のラインカラーは青 |
StationViewModel.swift | JSON ファイルから取得したデータを画面に渡す | |
KobeLineApp.swift | アプリを実行すると、まず実行する(エントリーポイント) | |
ContentView.swift | アプリの画面を定義 | |
StationNumberView.swift | 駅番号に関するビューを定義 | |
StationView.swift | 駅名に関するビューを定義 |
クラス図
クラス図を観察するとわかること
矢印 (→) の方向が一方方向であること
-
JSONLoader
はStationViewModel
のことを知らない -
StationViewModel
はKobeLineApp
のことを知らない ... 以下同様
考えること
- 現在は JSON ファイルからデータを取得し、解析している。データの取得先をデータベースに変更する際に発生する問題はあるか
- 現在は 神戸線のみ JSON ファイルからデータを取得している。仮に、宝塚線・京都線のデータも表示したいとする。その拡張にその変更に耐えられるか
現時点での反省点
- アプリの名前は
KobeLineApp
にしない方が良かった。理由:今後、宝塚線・京都線のデータも表示するかもしれないから
コード
Station_Kobe.json
[
{
"id": "kobe01",
"numbering": "01",
"line": "Kobe",
"name": "大阪梅田",
"nameEnglish": "Osaka-umeda",
"latitude": "34.705326",
"longitude": "135.498398",
"isFavorite": true
},
{
"id": "kobe02",
"numbering": "02",
"line": "Kobe",
"name": "中津",
"nameEnglish": "Nakatsu",
"latitude": "34.709851",
"longitude": "135.492499",
"isFavorite": false
},
{
"id": "kobe03",
"numbering": "03",
"line": "Kobe",
"name": "十三",
"nameEnglish": "Juso",
"latitude": "34.72049",
"longitude": "135.482198",
"isFavorite": true
},
{
"id": "kobe04",
"numbering": "04",
"line": "Kobe",
"name": "神崎川",
"nameEnglish": "Kanzakigawa",
"latitude": "34.732349",
"longitude": "135.472782",
"isFavorite": false
},
{
"id": "kobe05",
"numbering": "05",
"line": "Kobe",
"name": "園田",
"nameEnglish": "Sonoda",
"latitude": "34.751903",
"longitude": "135.448145",
"isFavorite": false
},
{
"id": "kobe06",
"numbering": "06",
"line": "Kobe",
"name": "塚口",
"nameEnglish": "Tsukaguchi",
"latitude": "34.752968",
"longitude": "135.416376",
"isFavorite": false
},
{
"id": "kobe07",
"numbering": "07",
"line": "Kobe",
"name": "武庫之荘",
"nameEnglish": "Mukonoso",
"latitude": "34.751596",
"longitude": "135.393428",
"isFavorite": false
},
{
"id": "kobe08",
"numbering": "08",
"line": "Kobe",
"name": "西宮北口",
"nameEnglish": "Nishinomiya-kitaguchi",
"latitude": "34.745956",
"longitude": "135.356597",
"isFavorite": true
},
{
"id": "kobe09",
"numbering": "09",
"line": "Kobe",
"name": "夙川",
"nameEnglish": "Shukugawa",
"latitude": "34.742262",
"longitude": "135.328128",
"isFavorite": true
},
{
"id": "kobe10",
"numbering": "10",
"line": "Kobe",
"name": "芦屋川",
"nameEnglish": "Ashiyagawa",
"latitude": "34.736568",
"longitude": "135.300937",
"isFavorite": false
},
{
"id": "kobe11",
"numbering": "11",
"line": "Kobe",
"name": "岡本",
"nameEnglish": "Okamoto",
"latitude": "34.729151",
"longitude": "135.275827",
"isFavorite": true
},
{
"id": "kobe12",
"numbering": "12",
"line": "Kobe",
"name": "御影",
"nameEnglish": "Mikage",
"latitude": "34.724559",
"longitude": "135.252254",
"isFavorite": false
},
{
"id": "kobe13",
"numbering": "13",
"line": "Kobe",
"name": "六甲",
"nameEnglish": "Rokko",
"latitude": "34.719652",
"longitude": "135.23429",
"isFavorite": false
},
{
"id": "kobe14",
"numbering": "14",
"line": "Kobe",
"name": "王子公園",
"nameEnglish": "Ojikoen",
"latitude": "34.710262",
"longitude": "135.218527",
"isFavorite": false
},
{
"id": "kobe15",
"numbering": "15",
"line": "Kobe",
"name": "春日野道",
"nameEnglish": "Kasuganomichi",
"latitude": "34.703103",
"longitude": "135.205507",
"isFavorite": false
},
{
"id": "kobe16",
"numbering": "16",
"line": "Kobe",
"name": "神戸三宮",
"nameEnglish": "Kobe-sannnomiya",
"latitude": "34.693143",
"longitude": "135.192847",
"isFavorite": true
}
]
JSONLoader.swift
import Foundation
class JSONLoader {
private static var instance: JSONLoader = JSONLoader()
private init(){}
public static func getInstance() -> JSONLoader { return JSONLoader.instance }
func load<T: Decodable>(_ filename: String) throws -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
throw JSONLoaderError.cantFindFile
}
do {
data = try Data(contentsOf: file)
} catch {
throw JSONLoaderError.cantLoadFile
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
throw JSONLoaderError.cantParseFile
}
}
enum JSONLoaderError:Error {
case cantFindFile
case cantLoadFile
case cantParseFile
}
}
Station.swift
import Foundation
import SwiftUI
struct Station: Hashable, Codable,Identifiable {
let id, numbering: String
let line: Line
let name, nameEnglish, latitude, longitude: String
let isFavorite: Bool
enum Line: String, Codable {
case kobe = "Kobe"
}
}
Line.swift
import Foundation
enum Line: String {
case Kobe
var description: String {
switch(self){
case .Kobe: return "神戸線"
}
}
var color: Color {
switch(self){
case .Kobe: return .blue
}
}
}
StationViewModel.swift
import Foundation
import SwiftUI
class StationViewModel: ObservableObject {
private var loader: JSONLoader = JSONLoader.getInstance()
@Published var stations: [Station] = []
init(){
do {
stations = try loader.load("Station_Kobe.json")
} catch {
stations = []
}
}
}
KobeLineApp.swift
import SwiftUI
@main
struct KobeLineApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: StationViewModel())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: StationViewModel
init(viewModel: StationViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
NavigationView {
listView // ネストが深くなるため listView に分けた
}
}
}
private var listView: some View {
List(viewModel.stations){ station in
HStack {
StationNumberView(station: station)
StationView(station: station)
}
}
}
}
StationView.swift
import SwiftUI
struct StationView: View {
var station: Station
var body: some View {
VStack(alignment: .leading) {
Text(station.name)
.font(.body)
.fontWeight(.bold)
Text(station.nameEnglish)
.font(.caption)
}
}
}
StationNumberView.swift
import SwiftUI
struct StationNumberView: View {
var station:Station
var body: some View {
ZStack{
Circle()
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 30,height: 30)
.foregroundColor(Line.Kobe.color)
VStack(spacing: 0) {
Text("HK")
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(Line.Kobe.color)
.padding(0)
Text(station.numbering)
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(Line.Kobe.color)
.padding(0)
}
}
}
}
参考資料
SwiftUI チュートリアル
阪急電鉄