- 投稿日:2019-05-02T20:25:46+09:00
【Swift5】UICollectionViewLayoutのサブクラスでHeaderとFooterを生成する
はじめに
UICollectionViewのレイアウトをUICollectionViewLayoutで作る機会があったため、その場合のHeaderとFooterの追加方法を整理しました。
完成イメージ
手順
UICollectionViewLayoutのサブクラスで生成した以下のレイアウトに対してHeaderとFooterを追加していきます。
※ 最終的なソースはこちらにあります。Headerの追加
追加するHeader用のUICollectionReusableViewクラスのサブクラスを用意(今回はXibファイルも合わせて用意したため、collectionView側に登録も行います)
CustomHeaderView.swiftclass CustomHeaderView: UICollectionReusableView {}ViewController.swiftcollectionView.register(UINib(nibName: String(describing: CustomHeaderView.self), bundle: .main), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: CustomHeaderView.self))UICollectionViewDataSourceにて、Header生成のタイミングで用意したサブクラスを返します。(Footerを追加する場合もこのタイミングで返すようにします)
ViewController.swiftfunc collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { if kind == UICollectionView.elementKindSectionHeader, let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: CustomHeaderView.self), for: indexPath) as? CustomHeaderView { return headerView } return UICollectionReusableView() }ここまでではまだHeaderは表示されないので、UICollectionViewLayoutのサブクラス側で、HeaderのUICollectionViewLayoutAttributesを生成して、レイアウトに追加する処理を行います。
※ CellのAttributes生成のタイミングではHeaderの高さ分の表示スペースを考慮するように注意してください。CustomLayout.swiftprivate func headerAttributes() { guard let collectionView = collectionView else { return } let indexPath = IndexPath(item: 0, section: 0) let headerViewAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: indexPath) headerViewAttribute.frame = CGRect(x: 0, y: 0, width: collectionView.bounds.size.width, height: 80) // 生成したLayoutAttributesを管理する配列にheader要素を追加 cachedAttributes.append(headerViewAttribute) // headerの高さ分のContentSizeを追加 contentHeight = max(contentHeight, headerViewAttribute.frame.maxY) }Footerの追加
Headerと同じ流れでFooterも追加できます。
CustomFooterView.swiftclass CustomFooterView: UICollectionReusableView {}ViewController.swiftcollectionView.register(UINib(nibName: String(describing: CustomFooterView.self), bundle: .main), forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: String(describing: CustomFooterView.self))headerと同じタイミングでサブクラスをDataSourceに返すようにします。
ViewController.swiftfunc collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { switch kind { case UICollectionView.elementKindSectionHeader: return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomHeaderView.self), for: indexPath) as? CustomHeaderView ?? UICollectionReusableView() case UICollectionView.elementKindSectionFooter: return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomFooterView.self), for: indexPath) as? CustomFooterView ?? UICollectionReusableView() default: return UICollectionReusableView() } }CustomLayout.swiftprivate func footerAttributes() { guard let collectionView = collectionView else { return } let indexPath = IndexPath(item: 0, section: 0) let footerViewAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, with: indexPath) footerViewAttribute.frame = CGRect(x: 0, y: contentHeight, // header, cell などの要素分の高さ width: collectionView.bounds.size.width, height: 80) // 生成したLayoutAttributesを管理する配列にfooter要素を追加 cachedAttributes.append(footerViewAttribute) // footerの高さ分のContentSizeを追加 contentHeight = max(contentHeight, footerViewAttribute.frame.maxY) }今回はCollectionViewのsectionは0個でheaderやfooterは一つずつでしたが、section別に複数のheaderやfoooterが必要な場合は、UICollectionViewLayoutAttributesの生成時に渡すIndexPathで調整すればいいようです。
HeaderやFooterの高さをController側から渡してみる
上の例ではHeaderやFooterの高さはハードコーディングしていましたが、動的なレイアウトの変更も考慮して、Controller側から高さの値をUICollectionViewLayoutのサブクラスに渡すようにしてみます。
手順としては、UICollectionViewLayoutのサブクラス側でprotocolを用意してやり、Controller側からデリゲートで値を返すようにしてみます。
CustomLayout.swiftprotocol CustomLayoutDelegate: class { func headerViewHeight(_ indexPath: IndexPath) -> CGFloat func footerViewHeight(_ indexPath: IndexPath) -> CGFloat }CustomLayout.swiftclass CustomLayout: UICollectionViewLayout { weak var delegate: CustomLayoutDelegate? ~~~~ 以下省略 ~~~~~ }ViewController.swiftif let customLayout = collectionView.collectionViewLayout as? CustomLayout { PhotoListViewLayout.delegate = self }ViewController.swiftextension ViewController: CustomLayoutDelegate { func headerViewHeight(_ indexPath: IndexPath) -> CGFloat { // 状況に応じてHeaderの高さを返す return view.frame.size.height * 0.1 } func footerViewHeight(_ indexPath: IndexPath) -> CGFloat { // 状況に応じてFooterの高さを返す return view.frame.size.height * 0.1 } }Cotroller側で返した値を、Header、FooterのUICollectionViewLayoutAttributesを生成するタイミングで受け取って高さの値に使用します。
CustomLayout.swiftlet headerViewHeight = delegate?.headerViewHeight(indexPath) ?? 0 let footerViewHeight = delegate?.footerViewHeight(indexPath) ?? 0これでレイアウトが更新される度にHeaderやFooterの高さを動的に変更することができそうです。?
ソースコード
以下のリポジトリに最終版のソースを置いてあります。
https://github.com/ddd503/CollectionView-Header-Footer-Sample
- 投稿日:2019-05-02T16:10:01+09:00
型を使う意味を考える 〜型システムでビジネスルールを捕捉する〜
ビジネスには多くのルール(仕様)があり
プログラムにもそれを反映させる必要があります。複雑な仕様があると
明確に何をしているのかが不明瞭になったり
思わぬミスをしてバグを生み出してしまう可能性があります。その際に
型システムを活用することで
仕様をコードにより明確に反映させたり
バグを生むリスクを減らすことができます。今回はどうやって型システムを活用するかについて
具体例から考えてみたいと思います。環境
言語: Swift5.0
IDE: Xcode10.2実装
今回は型を活用するということに焦点を当てているため
実装の詳細はかなり簡略にしております内容
あるサービスにユーザがいて
承認済みのメールアドレスと
未承認のメールアドレスを持つユーザがいるとします。※今回は簡略化のためメールアドレスにのみ着目しています。
運営側からユーザにメールを送りたいと考えおり
- 未承認のメールアドレスにはキャンペーンのメールを送る
- 承認済みのメールアドレスには秘密のメールを送る
という仕様があるとします。
最初の実装
struct UserMail { let address: EmailAddress let isApproved: Bool } struct EmailAddress { let address: String init(_ address: String) { self.address = address } } struct MailCheckService { func isApproved (_ user: UserMail) -> Bool { return user.isApproved } } // 未承認のメールにのみキャンペーンメールを送りたい func sendCampaignMail(user: UserMail, isApproved: (UserMail) -> Bool) { if !isApproved(user) { print("confirm mail send to \(user.address)!!") } } // 承認済みメールにのみ秘密のメールを送りたい func sendSecretMail(user: UserMail, isApproved: (UserMail) -> Bool) { if isApproved(user) { print("secret mail send to \(user.address)!!") } } func send() { let unApprovedUserMail = UserMail(address: EmailAddress("unApproved@hoge.com"), isApproved: false) let approvedUserMail = UserMail(address: EmailAddress("approved@hoge.com"), isApproved: true) let service = MailCheckService() // print("これは送る??♀️") sendCampaignMail(user: unApprovedUserMail, isApproved: service.isApproved) // print("これは送らない??♀️") sendCampaignMail(user: approvedUserMail, isApproved: service.isApproved) // print("これは送る??♀️") sendSecretMail(user: approvedUserMail, isApproved: service.isApproved) // print("これは送らない??♀️") sendSecretMail(user: unApprovedUserMail, isApproved: service.isApproved) }この場合では
メールアドレスの承認・未承認をisApproved
というプロパティで判定をしています。これでも正しく実装できていれば問題ありません。
しかしいくつか不安な点があります。
isApproved
では承認済みのメールアドレスと未承認のメールアドレスが存在することが明白に表現できてなかったり仕様が読み取りづらい(特に初見の開発者や開発者以外の人にとって)開発者が
isApproved
の設定ロジックを謝って
間違ったメールを送ってしまうリスクがある
(例えばメールアドレスが変更された場合にisApproved
もオフにしなければいけないのに忘れていたなど)型システムを活用した実装
これを型システムを活用して変更してみたいと思います。
UserMailをenumにする
enum UserMail { case unApproved(EmailAddress) case approved(ApprovedEmailAddress) // approvedを初期化できないようにする init(_ mail: EmailAddress) { self = .unApproved(mail) } } struct EmailAddress { let address: String init(_ address: String) { self.address = address } } // approved用のクラス struct ApprovedEmailAddress { let address: String init(_ address: String) { self.address = address } }
isApproved
の判定をenumで表現するようにしました。
こうすることで未承認と承認済みのメールアドレスがあることが明示できています。さらに承認済みメールアドレス(
EmailAddress
)と
未承認メールアドレス(ApprovedEmailAddress
)をクラスで分け
UserMailのイニシャライザではEmailAddress
しか受け取らず
未承認のケース(unApproved
)しか作成できないようにしています。こうすることで
承認していないのにも関わらず
承認済みのケース(approved
)を作成してしまうことを防ぐことができます。承認サービスを通してのみapproveできるようにする
approved
にするためには承認サービスを通すようにします。struct MailApproveService { func approveMail(mail: EmailAddress) -> UserMail? { // 何かの承認チェックをする...失敗した場合はnilになる return .approved(ApprovedEmailAddress(mail.address)) } }下記のような形で使用します。
let unApprovedUserMail = UserMail.unApproved(EmailAddress("user@hoge.com")) // これはできない??♀️ //let approvedUserMail = UserMail.approved(VerifiedEmailAddress("approved@hoge.com")) let service = MailApproveService() guard case .unApproved(let mail) = unApprovedUserMail, let approvedUserMail = service.approveMail(mail: mail) else { return } // UserMail.approved(ApprovedEmailAddress("user@hoge.com")) print(approvedUserMail)これが生み出すメリットとして
- 承認を通さなければ承認済みのUserMailが作成できないことを保証できる
- 承認メソッドの引数が
EmailAddress
なので承認済みメールアドレスを二重で承認することがなくなるなどがあります。
送信メソッドの引数で制限をかける
さらにメールを送信する方法も下記のように変更します。
func sendCampaignMail(mail: EmailAddress) { print("campaign mail send to \(mail.address)!!") } func sendSecretMail(mail: ApprovedEmailAddress) { print("secret mail send to \(mail.address)!!!!") }こうすることで
- 送るべきメールアドレスが引数で決まっているため謝って違う種類のメールを送るリスクが減る
- 何を設定すれば良いのかを明示的に示すことができる
使用すると下記のような結果になります。
if case .unApproved(let mail) = unApprovedUserMail { print("ここは通る??♀️") sendCampaignMail(mail: mail) } if case .approved(let mail) = unApprovedUserMail { print("ここは通らない??♀️") sendSecretMail(mail: mail) } if case .approved(let mail) = approvedUserMail { print("ここは通る??♀️") sendSecretMail(mail: mail) } if case .unApproved(let mail) = approvedUserMail { print("ここは通らない??♀️") service.approveMail(mail: mail) } if case .unApproved(let mail) = approvedUserMail { print("ここは通らない??♀️") sendCampaignMail(mail: mail) }別の例
もう一つ具体例を考えてみます。
内容
ユーザの入力データからユーザを登録する処理を考えます
その際に
- 名前は必ず入力する(空文字は??♀️)
- 連絡先として電話番号かメールアドレスいずれかが必須
という仕様があるとします。
最初の実装
struct PhoneNumber { let number: Int init(_ number: Int) { self.number = number } } struct MailAddress { let address: String init(_ address: String) { self.address = address } } struct Input { let name: String let address: MailAddress? let phoneNumber: PhoneNumber? } struct RegisterService { func register(_ input: Input) -> Bool { if name.isEmpty { return false } if input.address == nil && input.phoneNumber == nil { return false } else { // 何かの登録処理...失敗したらfalse return true } } } func register() { let registerService = RegisterService() let valid = registerService.register( Input(name: "hoge", address: MailAddress("hoge@hoge.com"), phoneNumber: PhoneNumber (123))) if valid { print("登録完了??♀️") } let inValid = registerService.register( Input(name: "hoge", address: nil, phoneNumber: nil)) if !inValid { print("登録失敗??♀️") } }この場合
- 入力値のチェックをしなければならない
- ありえないケースも考慮しなければならない(
PhoneNumber
とMailAddress
の両方がnil)- どういうケースがあるのかが不明瞭
- 開発者がケースを見落とす可能性がある(
PhoneNumber
とMailAddress
の両方が入力されている場合もOK)といった懸念点が挙げられます。
型システムを活用した実装
これを型システムを活用して変更してみたいと思います。
Contactをenumにする
enum Contact { case both(MailAddress, PhoneNumber) case address(MailAddress) case phoneNumber(PhoneNumber) var phoneNumber: PhoneNumber? { switch self { case .both(_, let p): return p case .address: return nil case .phoneNumber(let p): return p } } var mailAddress: MailAddress? { switch self { case .both(let a, _): return a case .address(let a): return a case .phoneNumber: return nil } } }こうすることで
- すべてのケースが明確になる
- 開発者がケースを見落とすリスクが減る
- ありえないケースを排除できる
というメリットがあります。
空文字が入らないStringの型を作る
Inputも変更します。
struct Input { let name: NonEmptyString let contact: Contact } struct NonEmptyString { let value: String init?(_ value: String) { if value.isEmpty { return nil } self.value = value } }ここでのポイントは
NonEmptyStringという型を作ることで
name
に空文字が入ることがなくなり
入力チェックが不要になります。registerから入力チェックをなくす
最後に登録メソッドを変更します。
struct RegisterService { func register(_ input: Input) -> Bool { let name: NonEmptyString = input.name let phone: PhoneNumber? = input.contact.phoneNumber let address: MailAddress? = input.contact.mailAddress // 何かの登録処理...失敗したらfalse return true } }ここでのメリットとして
- 不正な状態のInputを受け取らないので入力チェックが不要になる
- 不正な状態のInputを受け取らないので不正なデータが登録されるリスクが減る
まとめ
型システムを活用する方法を考えてみました。
この方法は定義する型の増加や
enumによる条件分岐が増加などにより
結果としてコード量が増えている場合が多いかもしれません。その代わりに
- 型でチェックができるのでより安全により安心した開発ができる
- 型を作成することで仕様がより明確に表現できるようになり初見の開発者や開発者以外の人でも仕様が理解しやすくなる
- ランタイムチェックでは必要であったテストケースが必要がなくなる
といったメリットを得ることができます。
特に仕様が明確になるという点に関しては
うまく型を作成することで
コードがドキュメントの役割も担ってくれるようになるので
大きなメリットになるなと感じています。すべての場合に型を作成していくことは
時間がかかることですし
不必要な部分もあると思います。どこまでやるかの塩梅はプロジェクトに依りますが
できる部分はできる限り活用できたらいいなと私は思っています。何か間違いなどございましたらご指摘いただけましたら幸いです
- 投稿日:2019-05-02T16:10:01+09:00
【Swift】型を使う意味を考える 〜型システムでビジネスルールを捕捉する〜
ビジネスには多くのルール(仕様)があり
プログラムにもそれを反映させる必要があります。複雑な仕様があると
明確に何をしているのかが不明瞭になったり
思わぬミスをしてバグを生み出してしまう可能性があります。その際に
型システムを活用することで
仕様をコードにより明確に反映させたり
バグを生むリスクを減らすことができます。今回はどうやって型システムを活用するかについて
具体例から考えてみたいと思います。環境
言語: Swift5.0
IDE: Xcode10.2実装
今回は型を活用するということに焦点を当てているため
実装の詳細はかなり簡略にしております内容
あるサービスにユーザがいて
承認済みのメールアドレスと
未承認のメールアドレスを持つユーザがいるとします。※今回は簡略化のためメールアドレスにのみ着目しています。
運営側からユーザにメールを送りたいと考えおり
- 未承認のメールアドレスにはキャンペーンのメールを送る
- 承認済みのメールアドレスには秘密のメールを送る
という仕様があるとします。
最初の実装
struct UserMail { let address: EmailAddress let isApproved: Bool } struct EmailAddress { let address: String init(_ address: String) { self.address = address } } struct MailCheckService { func isApproved (_ user: UserMail) -> Bool { return user.isApproved } } // 未承認のメールにのみキャンペーンメールを送りたい func sendCampaignMail(user: UserMail, isApproved: (UserMail) -> Bool) { if !isApproved(user) { print("confirm mail send to \(user.address)!!") } } // 承認済みメールにのみ秘密のメールを送りたい func sendSecretMail(user: UserMail, isApproved: (UserMail) -> Bool) { if isApproved(user) { print("secret mail send to \(user.address)!!") } } func send() { let unApprovedUserMail = UserMail(address: EmailAddress("unApproved@hoge.com"), isApproved: false) let approvedUserMail = UserMail(address: EmailAddress("approved@hoge.com"), isApproved: true) let service = MailCheckService() // print("これは送る??♀️") sendCampaignMail(user: unApprovedUserMail, isApproved: service.isApproved) // print("これは送らない??♀️") sendCampaignMail(user: approvedUserMail, isApproved: service.isApproved) // print("これは送る??♀️") sendSecretMail(user: approvedUserMail, isApproved: service.isApproved) // print("これは送らない??♀️") sendSecretMail(user: unApprovedUserMail, isApproved: service.isApproved) }この場合では
メールアドレスの承認・未承認をisApproved
というプロパティで判定をしています。これでも正しく実装できていれば問題ありません。
しかしいくつか不安な点があります。
isApproved
では承認済みのメールアドレスと未承認のメールアドレスが存在することが明白に表現できてなかったり仕様が読み取りづらい(特に初見の開発者や開発者以外の人にとって)開発者が
isApproved
の設定ロジックを謝って
間違ったメールを送ってしまうリスクがある
(例えばメールアドレスが変更された場合にisApproved
もオフにしなければいけないのに忘れていたなど)型システムを活用した実装
これを型システムを活用して変更してみたいと思います。
UserMailをenumにする
enum UserMail { case unApproved(EmailAddress) case approved(ApprovedEmailAddress) // approvedを初期化できないようにする init(_ mail: EmailAddress) { self = .unApproved(mail) } } struct EmailAddress { let address: String init(_ address: String) { self.address = address } } // approved用のクラス struct ApprovedEmailAddress { let address: String init(_ address: String) { self.address = address } }
isApproved
の判定をenumで表現するようにしました。
こうすることで未承認と承認済みのメールアドレスがあることが明示できています。さらに承認済みメールアドレス(
EmailAddress
)と
未承認メールアドレス(ApprovedEmailAddress
)をクラスで分け
UserMailのイニシャライザではEmailAddress
しか受け取らず
未承認のケース(unApproved
)しか作成できないようにしています。こうすることで
承認していないのにも関わらず
承認済みのケース(approved
)を作成してしまうことを防ぐことができます。承認サービスを通してのみapproveできるようにする
approved
にするためには承認サービスを通すようにします。struct MailApproveService { func approveMail(mail: EmailAddress) -> UserMail? { // 何かの承認チェックをする...失敗した場合はnilになる return .approved(ApprovedEmailAddress(mail.address)) } }下記のような形で使用します。
let unApprovedUserMail = UserMail.unApproved(EmailAddress("user@hoge.com")) // これはできない??♀️ //let approvedUserMail = UserMail.approved(VerifiedEmailAddress("approved@hoge.com")) let service = MailApproveService() guard case .unApproved(let mail) = unApprovedUserMail, let approvedUserMail = service.approveMail(mail: mail) else { return } // UserMail.approved(ApprovedEmailAddress("user@hoge.com")) print(approvedUserMail)これが生み出すメリットとして
- 承認を通さなければ承認済みのUserMailが作成できないことを保証できる
- 承認メソッドの引数が
EmailAddress
なので承認済みメールアドレスを二重で承認することがなくなるなどがあります。
送信メソッドの引数で制限をかける
さらにメールを送信する方法も下記のように変更します。
func sendCampaignMail(mail: EmailAddress) { print("campaign mail send to \(mail.address)!!") } func sendSecretMail(mail: ApprovedEmailAddress) { print("secret mail send to \(mail.address)!!!!") }こうすることで
- 送るべきメールアドレスが引数で決まっているため謝って違う種類のメールを送るリスクが減る
- 何を設定すれば良いのかを明示的に示すことができる
使用すると下記のような結果になります。
if case .unApproved(let mail) = unApprovedUserMail { print("ここは通る??♀️") sendCampaignMail(mail: mail) } if case .approved(let mail) = unApprovedUserMail { print("ここは通らない??♀️") sendSecretMail(mail: mail) } if case .approved(let mail) = approvedUserMail { print("ここは通る??♀️") sendSecretMail(mail: mail) } if case .unApproved(let mail) = approvedUserMail { print("ここは通らない??♀️") service.approveMail(mail: mail) } if case .unApproved(let mail) = approvedUserMail { print("ここは通らない??♀️") sendCampaignMail(mail: mail) }別の例
もう一つ具体例を考えてみます。
内容
ユーザの入力データからユーザを登録する処理を考えます
その際に
- 名前は必ず入力する(空文字は??♀️)
- 連絡先として電話番号かメールアドレスいずれかが必須
という仕様があるとします。
最初の実装
struct PhoneNumber { let number: Int init(_ number: Int) { self.number = number } } struct MailAddress { let address: String init(_ address: String) { self.address = address } } struct Input { let name: String let address: MailAddress? let phoneNumber: PhoneNumber? } struct RegisterService { func register(_ input: Input) -> Bool { if name.isEmpty { return false } if input.address == nil && input.phoneNumber == nil { return false } else { // 何かの登録処理...失敗したらfalse return true } } } func register() { let registerService = RegisterService() let valid = registerService.register( Input(name: "hoge", address: MailAddress("hoge@hoge.com"), phoneNumber: PhoneNumber (123))) if valid { print("登録完了??♀️") } let inValid = registerService.register( Input(name: "hoge", address: nil, phoneNumber: nil)) if !inValid { print("登録失敗??♀️") } }この場合
- 入力値のチェックをしなければならない
- ありえないケースも考慮しなければならない(
PhoneNumber
とMailAddress
の両方がnil)- どういうケースがあるのかが不明瞭
- 開発者がケースを見落とす可能性がある(
PhoneNumber
とMailAddress
の両方が入力されている場合もOK)といった懸念点が挙げられます。
型システムを活用した実装
これを型システムを活用して変更してみたいと思います。
Contactをenumにする
enum Contact { case both(MailAddress, PhoneNumber) case address(MailAddress) case phoneNumber(PhoneNumber) var phoneNumber: PhoneNumber? { switch self { case .both(_, let p): return p case .address: return nil case .phoneNumber(let p): return p } } var mailAddress: MailAddress? { switch self { case .both(let a, _): return a case .address(let a): return a case .phoneNumber: return nil } } }こうすることで
- すべてのケースが明確になる
- 開発者がケースを見落とすリスクが減る
- ありえないケースを排除できる
というメリットがあります。
空文字が入らないStringの型を作る
Inputも変更します。
struct Input { let name: NonEmptyString let contact: Contact } struct NonEmptyString { let value: String init?(_ value: String) { if value.isEmpty { return nil } self.value = value } }ここでのポイントは
NonEmptyStringという型を作ることで
name
に空文字が入ることがなくなり
入力チェックが不要になります。registerから入力チェックをなくす
最後に登録メソッドを変更します。
struct RegisterService { func register(_ input: Input) -> Bool { let name: NonEmptyString = input.name let phone: PhoneNumber? = input.contact.phoneNumber let address: MailAddress? = input.contact.mailAddress // 何かの登録処理...失敗したらfalse return true } }ここでのメリットとして
- 不正な状態のInputを受け取らないので入力チェックが不要になる
- 不正な状態のInputを受け取らないので不正なデータが登録されるリスクが減る
まとめ
型システムを活用する方法を考えてみました。
この方法は定義する型の増加や
enumによる条件分岐が増加などにより
結果としてコード量が増えている場合が多いかもしれません。その代わりに
- 型でチェックができるのでより安全により安心した開発ができる
- 型を作成することで仕様がより明確に表現できるようになり初見の開発者や開発者以外の人でも仕様が理解しやすくなる
- ランタイムチェックでは必要であったテストケースが必要がなくなる
といったメリットを得ることができます。
特に仕様が明確になるという点に関しては
うまく型を作成することで
コードがドキュメントの役割も担ってくれるようになるので
大きなメリットになるなと感じています。すべての場合に型を作成していくことは
時間がかかることですし
不必要な部分もあると思います。どこまでやるかの塩梅はプロジェクトに依りますが
できる部分はできる限り活用できたらいいなと私は思っています。何か間違いなどございましたらご指摘いただけましたら幸いです
- 投稿日:2019-05-02T15:18:40+09:00
【Swift】!と?
はじめに
最近Swift…というかiOSアプリ開発を始めました。
今までpythonをメインに使ってきたので、慣れない部分が多々あります。
その中で一番引っかかった「!」と「?」についてまとめたいと思います。?
「?」をつけるとOptional型になります。
Optional型とは、nil(pythonならNone、javaならNull)が代入可能な型のことです。
「?」をつけてOptional型にすることを「ラップする」と言います。test.swiftvar value: String? // valueという変数にはnil、またはStringが入る上記のように宣言すると、valueという変数にはString、またはnilを入れることができます。
「nilが入るかもしれないよ」という意味で理解しています。ラップした変数から値を取り出すことを「アンラップする」と言います。
アンラップは以下のように行います。test.swiftvar value: String? // valueという変数にはnil、またはStringが入る value = "Hello, World" // Stringを代入したけど、まだOptional型 if let value = value { // アンラップ // valueがnilでない時にここの処理が行われる // valueには"Hello, World"というString型が入っている }!
Optional型に「!」をつけると、強制的にアンラップすることができます。
test.swiftvar value: String? // valueという変数にはnil、またはStringが入る value = "Hello, World" // Stringを代入したけど、まだOptional型 print(value) // 出力は「Optional("Hello, World")」(Optional型) print(value!) // 出力は「Hello, World」(String型)「この変数はOptional型だけど、絶対に値は入ってる」という意味で理解をしています。
ちなみに、上記の例でnilのままだと、アンラップしようとした時点でエラーが生じます。test.swiftvar value: String? // valueという変数にはnil、またはStringが入る print(value) // 出力は「nil」 print(value!) // Fatal error: Unexpectedly found nil while unwrapping an Optional valueなんであるのか
かの有名な「ぬるぽ」(NullPointerException)を回避するためだと思っています。
変数に自由にnullを代入することが可能な言語では、予期せぬタイミングでnullが入っていた場合、エラーを生じることが多々あります(Nullへの演算等)。
SwiftではOptional型以外でnilを代入できなくすることで、予期せぬnilを防ぎ、エラーを防いでいます。最後に
まだ開発を始めたばかりで、慣れたとは言い難いですが、楽しくやっていきます。
誤り、指摘等ございましたらコメントをお願いいたします。参考
- 投稿日:2019-05-02T13:59:32+09:00
React Nativeで複数のstyleを適用する
私があたったユースケース的には
「H3のText用Styleと、リンクを有効化したっぽいStyleを2つ適用したい」
でした。一言で言うと「
style={}
には配列で複数のスタイルを渡すことができる」です。このようにします。
App.tsx<Text style={[styles.textH3, styles.textLink]}>ほげほげ</Text> //... const styles = StyleSheet.create({ textH3: { paddingTop: 10, paddingBottom: 10, fontSize: 20, fontWeight: 'bold', }, textLink: { color: 'blue' } });思っていたより簡単でした!
ちなみに(ちょっと注意点)
パラメータは後勝ちのようです。
上記の場合、例えばtextLinkにfontSize: 5
を入力すると、文字サイズが小さくなってしまいます。
注意しましょう!
- 投稿日:2019-05-02T13:36:50+09:00
iOSのヘルスケアデータをCSVに変換する
iOS(iPhone)やApple Watchで溜めているヘルスケアデータを、
再利用するためにデータ出力をしたが、xml形式で使いにくかったのでCSVに変換してみようというお話。当方環境
- iPhone7 iOS 12.2
- Apple Watch gen3(だった気がする)
やり方
- iPhoneのHealthアプリケーションから、
Export Health Darta
を選択し、zipファイルをエクスポートさせる。⇨しばらくするとファイルをどうするか聞かれるので、任意の方法でPCに送る
当方はmacでiCloud連携を使用してデスクトップにおきました。
zipファイルを解凍する
- ZIPファイルを解凍すると以下のようなディレクトリ構成になっています。
. ├── README.md ├── apple_health_export │ ├── export.xml │ └── export_cda.xml
- export_cda.xmlは中身がよくわからなかったので無視。
以下、編集するためのプログラムです。
package main import ( "encoding/xml" "io/ioutil" "log" "os" "github.com/gocarina/gocsv" ) type HealthData struct { XMLName xml.Name `xml:"HealthData"` Record []Record } type Record struct { Type string `xml:"type,attr" csv:"type"` SourceName string `xml:"sourceName,attr" csv:"-"` SourceVersion string `xml:"sourceVersion,attr" csv:"-"` Unit string `xml:"unit,attr" csv:"unit"` CreationDate string `xml:"creationDate,attr" csv:"-"` StartDate string `xml:"startDate,attr" csv:"startDate"` EndDate string `xml:"endDate,attr" csv:"endDate"` Value string `xml:"value,attr" csv:"value"` } func main() { // open xml file. xmlFile, err := os.Open("./apple_health_export/export.xml") if err != nil { log.Fatal(err) } defer xmlFile.Close() // read xml data. xmlData, err := ioutil.ReadAll(xmlFile) if err != nil { log.Fatal(err) } // parse xml and export to csv. csvFile, err := os.Create("./export.csv") if err != nil { log.Fatal(err) } defer csvFile.Close() var healthData HealthData xml.Unmarshal(xmlData, &healthData) records := healthData.Record gocsv.MarshalFile(&records, csvFile) }ポイント
HealthData内にRecordとして複数データが含まれているデータ構造だったので、
HealthData構造体とRecord構造体を用意しました。Record構造体から抜き出す項目は
csv:"xxx"
として記述し、
抽出不要項目はcsv:"-"
とすることでparserが良い感じに処理してくれます。改善ポイント
- メモリ消費が非常に大きい
- ファイルサイズが大きいとそれに比例してメモリ使用量が増える
最後に
走り書きですが、とりあえず使えそうなデータが取れたので、
CSVを使って何かできないか試してみたいと思います。
- 投稿日:2019-05-02T10:34:58+09:00
Swiftのenumはprotocolに準拠できるので、例えばComparableによってシンプルに比較できる
Swiftのenumは、protocolに準拠させることで、いろいろな振る舞いを与えることができます。
これは実務では強力な機能であり、ちょっとした工夫によってコードをシンプルにできたりします。例えば以下のようなことができます。
- Comparableに準拠することで、比較演算子を定義したり
- CaseIterableに準拠することで、allCases プロパティを通して全ての値をコレクションとして取得できたり
コードサンプルで表現するとこんな感じです。
Playgroundimport Foundation /// Tシャツサイズ enum TshirtSize: String, Comparable, CaseIterable { case xs = "XS" case s = "S" case m = "M" case l = "L" case xl = "XL" case xxl = "XXL" // Comparableに準拠すると、この関数を定義しなければならない(定義することで大小比較が可能になる) static func < (lhs: TshirtSize, rhs: TshirtSize) -> Bool { return lhs.order < rhs.order } var order: Int { // CaseIterableに準拠すると、allCasesで全caseの配列を取得できる return TshirtSize.allCases.firstIndex(of: self) ?? 0 } } let m = TshirtSize.m let l = TshirtSize.l let xl = TshirtSize.xl print("\(m < l)") // true print("\(l <= xl)") // true print("\(m > xl)") // false環境
- Xcode 10.1
- Swift 4.2
- 投稿日:2019-05-02T00:21:34+09:00
Flutterを使ってクソアプリを作ってみた
これは何?
最近、若者の間で「クソアプリ」なるものを作って公開するのが話題になっているようで。いや確かに「ちゃんとしたもの」を作ろうとすると、それはそれで多大なる労力が必要になってハードルも上がるのだけれども、クソアプリでいいやってハードルを下げると「ちょっとかじってみる」程度の経験を積むにはいいし、やったことを公開することで情報の整理にもつながっていい気がするな、ということで最近気になるFlutterで負けじとクソアプリを作ってみた。
ソースコードはコチラ。
Flutterとは?
Android/iOSのマルチデバイス対応のアプリが1つのソースコードで開発できるというGoogleが作った技術。Dart言語で実装する。
やったこと
開発環境の構築
大いにこの記事を参考にしており「開発環境の構築」は全く同じなので端折ります(手抜き)
Flutterに入門してクソアプリを作るまで
https://qiita.com/matsushou/items/f62ca1fb249670d7dbbcちなバージョンは
- Flutter v1.2.1 (for Mac)
- Android Studio 3.4
- FlutterとDartのプラグインを入れる
Dartに再入門する
筆者Google好きで、Dartが公開された直後(1.0以前)にかじったことがあったので、おさらい的に言語仕様(結構変わっている...)を見直してみる。
Language Tourをナナメ読む
https://www.dartlang.org/guides/language/language-tour前述の参考記事のとおり Important concepts まで読んだが、Javaエンジニアとしては以下を踏まえればどうにかなりそう。
- 型はある、クラス定義も継承もある、ちゃんとオブジェクト指向
new
は2.x系から省略可能になった(当時のリリースノート)- アンダースコア
_
始まりの変数/関数/クラスがprivateになる
- 可視性を示す修飾子 public/private はない
Flutterアプリを作ってみる
Android Studioから「Start a new Flutter project」を選択
- Flutter Applicationを選択してNext
- Project nameを適当に指定(デフォルトの
flutter_app
のままでもいい)してNext- Company domainを適当に指定、Sample Applicationの "generate sample content" にチェックしてFinish
- サンプルコード付きのプロジェクトが作成される
![]()
main.dart
にメインロジックが記載されている- ちゃんとテストコード
widget_test.dart
も用意されているのが素敵![]()
- Emulatorを用意して起動
- AVD Managerから端末のVMを用意して起動する
- +ボタンを押すと画面に表示された数値が増えていくサンプルアプリらしい
- サンプルアプリの画像は前述の記事にキャプチャがあるので、そっちを見てください(という手抜き)
アプリを実装する
クソアプリの境界線ってどこだろう?うっかりちゃんと作ってしまうとクソではなくなってしまうので、ここは慎重にクソアプリを作る必要がある。
今回は、macOSを始めとして Google Chrome や Evernote でも実装されている流行りの DarkMode を搭載すべく「ダークモードへの切り替え」をこのアプリに実装してみることにする。
漢は黙ってCI
とはいえロジックに手を出し始めるのではなく、眼の前に動くコードとテストコードがあるなら、まずCIを回さないとね。ということでCircleCIでのビルドを設定しておく。
- 生成されたサンプルプロジェクトをそのままコミット&GitHubへプッシュする。
次に挙げるサイトを参考にすると、すでにFlutter用のビルドコンテナが用意されているのが分かるので、単純に
.circleci/config.yml
を追加するだけで、CircleCI上でビルドが動くようになる。作業ログcircleci/config.ymlversion: 2 jobs: build: docker: - image: cirrusci/flutter steps: - checkout - run: flutter doctor - run: flutter test - run: flutter build -v apkこれで、branch push 時や、master マージ時にビルドが走るようになる。コケたら教えてくれるので、あんしんあんしん。
モード切り替え用のコントロール(Switch)を配置
よし、本筋に戻ってまずはスイッチを配置するぞ。ということでサンプルをググってみる。
https://kodestat.gitbook.io/flutter/24-flutter-switch
これがシンプルにまとまっていて分かりやすい。他にもサンプルケースが色々あって良さげ。このサイトはGitBookで作られていて、元のリポジトリは ここ のようだ。
main.dart
の内容をまるっとこのサンプルに書き換えて実行してみたら、ちゃんとスイッチのサンプルが表示された。そうそう、それよそれ。
今回はタイトル付きのスイッチを使いたいので、この部分+_onChanged2()
関数をパクればよろし。main.dartnew SwitchListTile( value: _value2, onChanged: _onChanged2, title: new Text('Hello World', style: new TextStyle(fontWeight: FontWeight.bold, color: Colors.red)), )実際には、こんなソースを
child: Column()
のchildren: <Widget>[]
の先頭に追加しました。
この時点ではまだ_changeDarkMode()
関数はテキトーです。main.dartContainer( padding: EdgeInsets.all(40.0), child: SwitchListTile( value: _isDarkMode, onChanged: _changeDarkMode, title: Text('Dark mode', style: new TextStyle(fontWeight: FontWeight.bold)), ), ),見た目はこんな感じに。
パーツを配置しただけで「Dark modeに切り替えられる感」が8割増しましたね。
いざ、DarkModeへ切り替え
残るは
_changeDarkMode()
関数の実装だ。ということで試しにbackgroundColor
指定だけblackで追加したら、文字は黒のままで画面全体が真っ黒になってしまったので、どうやら何か違うらしい。調べてみると Theme を切り替える必要がありそう。Using Themes to share colors and font styles - Flutter
でもこの実装を真面目にやると面倒くさいなぁ、と調べ続けていたら、
DynamicTheme
というドンピシャなwidgetが公開されていたので、これを使おう。
- How to Dynamic Theming in Flutter – Aswin Mohan – Medium
- How to dynamically change the theme in Flutter – ProAndroidDev
pubspec.yaml
のdependenciesに1行追加する。pubspec.yamldependencies: ...(中略) dynamic_theme: ^1.0.0
main.dart
の修正箇所はこんな感じ。(抜粋)main.dartimport 'package:dynamic_theme/dynamic_theme.dart'; ...(中略) class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return DynamicTheme( defaultBrightness: Brightness.light, data: (brightness) => ThemeData( primarySwatch: Colors.blue, brightness: brightness, ), themedWidgetBuilder: (context, theme) { return MaterialApp( title: 'Flutter Demo', theme: theme, home: MyHomePage(title: 'Flutter Dark Mode Sample'), ); } ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { bool _isDarkMode = false; void _changeDarkMode(bool value) { setState(() { _isDarkMode = value; var _brightness = Brightness.light; if (_isDarkMode) { _brightness = Brightness.dark; } DynamicTheme.of(context).setBrightness(_brightness); }); } ...(略) }
結果
- スイッチのOn/OffでThemeが動的に切り替わり、憧れのDarkModeに!ヒャッホウ!!
![]()
![]()
- うーん、、、冷静に見ると「だからどうした感」のあるクソアプリですね
![]()
- 参考:作業ログ
感想
- 簡単にスマホアプリが作れそう
- 今回はアプリを最終ビルドするところまでやっていないけど、簡単な動作確認レベルならすぐイケる
- dart はキャッチアップ楽ちん
- エディタの補完も効けば何の苦もない
- ソースコードの保存 → エミュレータへの自動反映、での動作確認が非常に快適
- クソアプリ作ってみるの楽しい
GW後半に軽くプログラミングしてみたい方は、いかがでしょうか?
お粗末さまでした。