・View、Presenter、Usecase、Repository、DataStoreに層を分けて実装している ・DataStoreのModelデータ←→Usecaseより前の層で扱うデータ(Valueとよんでおく)の変換はRepositoryで行う ・DataStoreへのデータ追加がある場合、View、Presenter、UsecaseのどこかでValueデータを作成する。今のところ、Usecaseで行うのがいいのかなと感じている ・Valueデータを作成にはValueCreatorプロトコルを用意し、Modelデータ←→ValueデータにはTranslatorプロトコルを用意している
Alamofireでのステータスコードとコンテンツタイプのハンドリング
はじめに
Alamofireではデフォルトではレスポンスの内容にかかわらず成功として処理される。
例えば下記のようなリクエストの場合、responseを取得できる。
AF.request("https://api.example.com").responseJSON { response in // handling response }
この中にはURLRequestやHTTPURLResponse、Dataなどが含まれている。
サーバーからのレスポンスをそのまま返し、Alamofireではステータスコードと コンテンツタイプを見てエラーかどうかは処理されない。
ステータスコードとコンテンツタイプのハンドリング
コールバックされるクロージャのなかで自分でハンドリングするのも良いのですが、 ステータスコードとコンテンツタイプには簡単にハンドリングするための方法がAlamofireで用意されている。
ステータスコード(status code)
validate(statusCode:)メソッドを使う。 指定したステータスコード以外の場合はAFError.responseValidationFailedエラーが返される。
AF.request("https://api.example.com") .validate(statusCode: 200..<300) .responseJSON { response in // handling response }
コンテンツタイプ(content type)
validate(contentType:)メソッドを使う。 指定したコンテンツタイプ以外の場合はAFError.responseValidationFailedエラーが返される。
AF.request("https://api.example.com") .validate(contentType: ["application/json"]) .responseJSON { response in // handling response }
デフォルト
引数なしのvalidate()メソッドを使う場合、 デフォルトのステータスコード(status code)とコンテンツタイプ(content type)をチェックしてくれる。
AF.request("https://api.example.com") .validate() .responseJSON { response in // handling response }
デフォルトの値は以下の通り。
fileprivate var acceptableStatusCodes: Range<Int> { 200..<300 } fileprivate var acceptableContentTypes: [String] { if let accept = request?.value(forHTTPHeaderField: "Accept") { return accept.components(separatedBy: ",") } return ["*/*"] }
【Swift】日付を含むJSONをCodableでDate型に変換する
はじめに
日付を含むJSONをCodableでDate型にする方法をメモしておく。 日付の表現は複数あります。
例えば、下記の表現があります。
- 2020-07-25
- 2020/07/25
- TimeInterval
- その他
変換したいJSONで使われている日付表現に合わせて、設定する必要があります。
Date型への変換
JSONを変換するときに使用するJSONDecoderはデフォルトでは00:00:00 UTC on 1 January 2001からのTimeInterval形式(数値)を変換できます。
その他の日付表現を変換する場合はdateDecodingStrategyプロパティでどのようなフォーマットなのかを指定する必要があります。
デフォルトの変換形式
00:00:00 UTC on 1 January 2001からのTimeInterval形式(数値)を変換できます。
JSONで指定されるTimeIntervalは
00:00:00 UTC on 1 January 2001
からの経過秒数である必要があります。 おそらくデフォルトではDate型の
init(timeIntervalSinceReferenceDate ti: TimeInterval)
https://developer.apple.com/documentation/foundation/nsdate/1409769-init
を使って変換しているのだと思います。
サンプル
import Foundation let jsonString = """ { "createdAt": 617339567.66283596 } """ struct DateObject: Codable { let createdAt: Date } let decoder = JSONDecoder() let result = try decoder.decode(DateObject.self, from: jsonString.data(using: .utf8)!) print(result.createdAt) // 2020-07-25 03:12:47 +0000
ISO8601
dateDecodingStrategyでiso8601を指定します。
サンプル
import Foundation let jsonString = """ { "createdAt": "2020-07-25T03:12:47-00:00" } """ struct DateObject: Codable { let createdAt: Date } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let result = try decoder.decode(DateObject.self, from: jsonString.data(using: .utf8)!) print(result.createdAt) // 2020-07-25 03:12:47 +0000
UnixTime(秒指定)
dateDecodingStrategyでsecondsSince1970を指定します。
サンプル
import Foundation let jsonString = """ { "createdAt": 1595646767 } """ struct DateObject: Codable { let createdAt: Date } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .secondsSince1970 let result = try decoder.decode(DateObject.self, from: jsonString.data(using: .utf8)!) print(result.createdAt) // 2020-07-25 03:12:47 +0000
UnixTime(ミリ秒指定)
dateDecodingStrategyでsecondsSince1970を指定します。
サンプル
import Foundation let jsonString = """ { "createdAt": 1595646767000 } """ struct DateObject: Codable { let createdAt: Date } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .millisecondsSince1970 let result = try decoder.decode(DateObject.self, from: jsonString.data(using: .utf8)!) print(result.createdAt) // 2020-07-25 03:12:47 +0000
その他文字列
dateDecodingStrategyでformattedに使用するフォーマットを加えて指定します。
サンプル
import Foundation let jsonString = """ { "createdAt": "2020-07-25 03:12:47+00:00" } """ struct DateObject: Codable { let createdAt: Date } let decoder = JSONDecoder() let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ssZ" decoder.dateDecodingStrategy = .formatted(formatter) let result = try decoder.decode(DateObject.self, from: jsonString.data(using: .utf8)!) print(result.createdAt) // 2020-07-25 03:12:47 +0000
【SwiftUI】一画面で複数のモーダルを出し分けする
はじめに
画面で2つの種類のモーダルを出すときにどのように実装したらよいかを考えました。(例えば、新規追加画面と編集画面など)
アプリ起動時
addボタンタップ時
editボタンタップ時
実装例
パターン1
モーダルを起動するボタンごとにsheet修飾子でモーダル表示をする。 デザインとアクションの実装が続き、読みにくい
struct ContentView: View { @State var isShowAddSheet = false @State var isShowEditSheet = false var body: some View { VStack { Text("Hello, World!") Button("add") { self.isShowAddSheet = true } .sheet(isPresented: $isShowAddSheet) { Text("add").background(Color.yellow) } Button("edit") { self.isShowEditSheet = true }.sheet(isPresented: $isShowEditSheet) { Text("edit").background(Color.green) } } } }
パターン2
デザインとアクションが分けられてソースを読みやすくなった気がするが、やりたいことを実現できないのでNG。Viewはsheetをひとつしか持てないようで、addはボタンを押してもモーダル表示されませんでした。editボタンは問題なくモーダル表示されました。
import SwiftUI struct ContentView: View { @State var isShowAddSheet = false @State var isShowEditSheet = false var body: some View { VStack { Text("Hello, World!") Button("add") { self.isShowAddSheet = true } Button("edit") { self.isShowEditSheet = true } } .sheet(isPresented: $isShowAddSheet) { Text("add").background(Color.yellow) } .sheet(isPresented: $isShowEditSheet) { Text("edit").background(Color.green) } } }
パターン3
- モーダル表示にはitem引数を持つ方のsheet修飾子を使う
- item引数には出し分けするモーダルを表すenumを作って使う
モーダル表示にはitem引数を持つ方のsheet修飾子を使う
こちらを使う
public func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Item) -> Content) -> some View where Item : Identifiable, Content : View
item引数には出し分けするモーダルを表すenumを作って使う
画面で新規追加画面と編集画面の2つを出すとします。
下記のようなenumを作成します。
enum SheetType: Int, Identifiable { case add case edit var id: Int { return self.rawValue } }
パターン3実装
import SwiftUI struct ContentView: View { @State var sheetType: SheetType? = nil var body: some View { VStack { Text("Hello, World!") Button("add") { self.sheetType = .add } Button("edit") { self.sheetType = .edit } } .background(Color.gray) .sheet(item: self.$sheetType) { (t) -> AnyView in self.showSheet(type: t) } } private func showSheet(type: SheetType) -> AnyView { switch type { case .add: return AnyView(Text("add").background(Color.yellow)) case .edit: return AnyView(Text("edit").background(Color.green)) } } enum SheetType: Int, Identifiable { case add case edit var id: Int { return self.rawValue } } }
実行
アプリ起動時
addボタンタップ時
editボタンタップ時
結論
他にもっといい方法があるかもしれないが、ひとまずenumを作ってやっておくのが良いとして実装した。
複数のViewを横並びにしていて、表示しきれない場合に折り返す表示を作る
はじめに
基本的には横並びにViewを並べたい。でも、iPhoneSEなどの横幅が小さい端末では表示しきれないので、その場合に折り返して、表示しきれなかったViewを表示したい。
たとえば、下記画像のような検索条件設定画面があるとします。
これはiPhone11の画像です。 検索条件で検索する商品のカテゴリを選択でき、選択したカテゴリが青色で表示されるとします。カテゴリ名が長いものがあったり、端末サイズが小さいものであった場合、表示しきれません。
端末サイズがiPhoneSEの場合は下記の画像のよう表示となります。
表示しきれない場合にフォントを小さくしたり、表示しきれない部分を...とするなどの方法はありますが、表示しきれない場合は折り返して表示するような実装を考えました。
結果として以下のようにします。
iPhone11の場合
iPhoneSEの場合
実装方法
表示しきれない場合に折り返して表示することはUICollectionViewで実現できます。ただし折り返してセルが表示される場合、セルの表示位置やセル間のスペースが期待通りにならないため、UICollectionViewFlowLayoutのサブクラスを作成し、実装する必要があります。
実装したUICollectionViewFlowLayoutは下記のとおりです。
import UIKit class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout { let cellSpacing: CGFloat = 28 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { self.minimumLineSpacing = 5 self.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var maxY: CGFloat = 0 attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.y >= maxY { leftMargin = sectionInset.left } layoutAttribute.frame.origin.x = leftMargin leftMargin += layoutAttribute.frame.width + cellSpacing maxY = max(layoutAttribute.frame.maxY, maxY) } return attributes } }
これをcollectionViewのcollectionViewLayoutプロパティに設定すればよい。
実装したサンプルはこちらにアップしています。 https://github.com/fuji2013/SampleNewlineView github.com