20190502のiOSに関する記事は8件です。

【Swift5】UICollectionViewLayoutのサブクラスでHeaderとFooterを生成する

はじめに

UICollectionViewのレイアウトをUICollectionViewLayoutで作る機会があったため、その場合のHeaderとFooterの追加方法を整理しました。

完成イメージ

Header Footer

手順

UICollectionViewLayoutのサブクラスで生成した以下のレイアウトに対してHeaderとFooterを追加していきます。
※ 最終的なソースはこちらにあります。

Start

Headerの追加

追加するHeader用のUICollectionReusableViewクラスのサブクラスを用意(今回はXibファイルも合わせて用意したため、collectionView側に登録も行います)

CustomHeaderView.swift
class CustomHeaderView: UICollectionReusableView {}
ViewController.swift
collectionView.register(UINib(nibName: String(describing: CustomHeaderView.self), bundle: .main),
                                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                                withReuseIdentifier: String(describing: CustomHeaderView.self))

UICollectionViewDataSourceにて、Header生成のタイミングで用意したサブクラスを返します。(Footerを追加する場合もこのタイミングで返すようにします)

ViewController.swift
func 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.swift
private 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)
}

これでHeaderは表示できました。
Header

Footerの追加

Headerと同じ流れでFooterも追加できます。

CustomFooterView.swift
class CustomFooterView: UICollectionReusableView {}
ViewController.swift
collectionView.register(UINib(nibName: String(describing: CustomFooterView.self), bundle: .main),
                        forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
                        withReuseIdentifier: String(describing: CustomFooterView.self))

headerと同じタイミングでサブクラスをDataSourceに返すようにします。

ViewController.swift
func 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.swift
private 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)
}

これでFooterの表示もできました。
Footer

今回はCollectionViewのsectionは0個でheaderやfooterは一つずつでしたが、section別に複数のheaderやfoooterが必要な場合は、UICollectionViewLayoutAttributesの生成時に渡すIndexPathで調整すればいいようです。

HeaderやFooterの高さをController側から渡してみる

上の例ではHeaderやFooterの高さはハードコーディングしていましたが、動的なレイアウトの変更も考慮して、Controller側から高さの値をUICollectionViewLayoutのサブクラスに渡すようにしてみます。

手順としては、UICollectionViewLayoutのサブクラス側でprotocolを用意してやり、Controller側からデリゲートで値を返すようにしてみます。

CustomLayout.swift
protocol CustomLayoutDelegate: class {
    func headerViewHeight(_ indexPath: IndexPath) -> CGFloat
    func footerViewHeight(_ indexPath: IndexPath) -> CGFloat
}
CustomLayout.swift
class CustomLayout: UICollectionViewLayout {

    weak var delegate: CustomLayoutDelegate?

    ~~~~ 以下省略 ~~~~~
}
ViewController.swift
if let customLayout = collectionView.collectionViewLayout as? CustomLayout {
            PhotoListViewLayout.delegate = self
        }
ViewController.swift
extension 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.swift
 let headerViewHeight = delegate?.headerViewHeight(indexPath) ?? 0
 let footerViewHeight = delegate?.footerViewHeight(indexPath) ?? 0

これでレイアウトが更新される度にHeaderやFooterの高さを動的に変更することができそうです。?

ソースコード

以下のリポジトリに最終版のソースを置いてあります。
https://github.com/ddd503/CollectionView-Header-Footer-Sample

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

型を使う意味を考える 〜型システムでビジネスルールを捕捉する〜

ビジネスには多くのルール(仕様)があり
プログラムにもそれを反映させる必要があります。

複雑な仕様があると
明確に何をしているのかが不明瞭になったり
思わぬミスをしてバグを生み出してしまう可能性があります。

その際に
型システムを活用することで
仕様をコードにより明確に反映させたり
バグを生むリスクを減らすことができます。

今回はどうやって型システムを活用するかについて
具体例から考えてみたいと思います。

環境

言語: Swift5.0
IDE: Xcode10.2

実装

今回は型を活用するということに焦点を当てているため
実装の詳細はかなり簡略にしております:bow_tone1:

内容

あるサービスにユーザがいて
承認済みのメールアドレスと
未承認のメールアドレスを持つユーザがいるとします。

※今回は簡略化のためメールアドレスにのみ着目しています。

運営側からユーザにメールを送りたいと考えおり

  • 未承認のメールアドレスにはキャンペーンのメールを送る
  • 承認済みのメールアドレスには秘密のメールを送る

という仕様があるとします。

最初の実装

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("登録失敗??‍♀️")
    }
}

この場合

  • 入力値のチェックをしなければならない
  • ありえないケースも考慮しなければならない(PhoneNumberMailAddressの両方がnil)
  • どういうケースがあるのかが不明瞭
  • 開発者がケースを見落とす可能性がある(PhoneNumberMailAddressの両方が入力されている場合も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による条件分岐が増加などにより
結果としてコード量が増えている場合が多いかもしれません。

その代わりに

  • 型でチェックができるのでより安全により安心した開発ができる
  • 型を作成することで仕様がより明確に表現できるようになり初見の開発者や開発者以外の人でも仕様が理解しやすくなる
  • ランタイムチェックでは必要であったテストケースが必要がなくなる

といったメリットを得ることができます。

特に仕様が明確になるという点に関しては
うまく型を作成することで
コードがドキュメントの役割も担ってくれるようになるので
大きなメリットになるなと感じています。

すべての場合に型を作成していくことは
時間がかかることですし
不必要な部分もあると思います。

どこまでやるかの塩梅はプロジェクトに依りますが
できる部分はできる限り活用できたらいいなと私は思っています。

何か間違いなどございましたらご指摘いただけましたら幸いです:bow_tone1:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】型を使う意味を考える 〜型システムでビジネスルールを捕捉する〜

ビジネスには多くのルール(仕様)があり
プログラムにもそれを反映させる必要があります。

複雑な仕様があると
明確に何をしているのかが不明瞭になったり
思わぬミスをしてバグを生み出してしまう可能性があります。

その際に
型システムを活用することで
仕様をコードにより明確に反映させたり
バグを生むリスクを減らすことができます。

今回はどうやって型システムを活用するかについて
具体例から考えてみたいと思います。

環境

言語: Swift5.0
IDE: Xcode10.2

実装

今回は型を活用するということに焦点を当てているため
実装の詳細はかなり簡略にしております:bow_tone1:

内容

あるサービスにユーザがいて
承認済みのメールアドレスと
未承認のメールアドレスを持つユーザがいるとします。

※今回は簡略化のためメールアドレスにのみ着目しています。

運営側からユーザにメールを送りたいと考えおり

  • 未承認のメールアドレスにはキャンペーンのメールを送る
  • 承認済みのメールアドレスには秘密のメールを送る

という仕様があるとします。

最初の実装

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("登録失敗??‍♀️")
    }
}

この場合

  • 入力値のチェックをしなければならない
  • ありえないケースも考慮しなければならない(PhoneNumberMailAddressの両方がnil)
  • どういうケースがあるのかが不明瞭
  • 開発者がケースを見落とす可能性がある(PhoneNumberMailAddressの両方が入力されている場合も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による条件分岐が増加などにより
結果としてコード量が増えている場合が多いかもしれません。

その代わりに

  • 型でチェックができるのでより安全により安心した開発ができる
  • 型を作成することで仕様がより明確に表現できるようになり初見の開発者や開発者以外の人でも仕様が理解しやすくなる
  • ランタイムチェックでは必要であったテストケースが必要がなくなる

といったメリットを得ることができます。

特に仕様が明確になるという点に関しては
うまく型を作成することで
コードがドキュメントの役割も担ってくれるようになるので
大きなメリットになるなと感じています。

すべての場合に型を作成していくことは
時間がかかることですし
不必要な部分もあると思います。

どこまでやるかの塩梅はプロジェクトに依りますが
できる部分はできる限り活用できたらいいなと私は思っています。

何か間違いなどございましたらご指摘いただけましたら幸いです:bow_tone1:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】!と?

はじめに

最近Swift…というかiOSアプリ開発を始めました。
今までpythonをメインに使ってきたので、慣れない部分が多々あります。
その中で一番引っかかった「!」と「?」についてまとめたいと思います。

?

「?」をつけるとOptional型になります。
Optional型とは、nil(pythonならNone、javaならNull)が代入可能な型のことです。
「?」をつけてOptional型にすることを「ラップする」と言います。

test.swift
var value: String? // valueという変数にはnil、またはStringが入る

上記のように宣言すると、valueという変数にはString、またはnilを入れることができます。
「nilが入るかもしれないよ」という意味で理解しています。

ラップした変数から値を取り出すことを「アンラップする」と言います。
アンラップは以下のように行います。

test.swift
var value: String? // valueという変数にはnil、またはStringが入る
value = "Hello, World" // Stringを代入したけど、まだOptional型

if let value = value { // アンラップ
    // valueがnilでない時にここの処理が行われる
    // valueには"Hello, World"というString型が入っている
}

!

Optional型に「!」をつけると、強制的にアンラップすることができます。

test.swift
var value: String? // valueという変数にはnil、またはStringが入る
value = "Hello, World" // Stringを代入したけど、まだOptional型
print(value) // 出力は「Optional("Hello, World")」(Optional型)
print(value!) // 出力は「Hello, World」(String型)

「この変数はOptional型だけど、絶対に値は入ってる」という意味で理解をしています。
ちなみに、上記の例でnilのままだと、アンラップしようとした時点でエラーが生じます。

test.swift
var 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を防ぎ、エラーを防いでいます。

最後に

まだ開発を始めたばかりで、慣れたとは言い難いですが、楽しくやっていきます。
誤り、指摘等ございましたらコメントをお願いいたします。

参考

swift公式
SwiftのOptional型を極める

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 を入力すると、文字サイズが小さくなってしまいます。
注意しましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSのヘルスケアデータをCSVに変換する

iOS(iPhone)やApple Watchで溜めているヘルスケアデータを、
再利用するためにデータ出力をしたが、xml形式で使いにくかったのでCSVに変換してみようというお話。

githubリポジトリ

当方環境

  • iPhone7 iOS 12.2
  • Apple Watch gen3(だった気がする)

やり方

  • iPhoneのHealthアプリケーションから、Export Health Dartaを選択し、zipファイルをエクスポートさせる。

IMG_1219.jpg

⇨しばらくするとファイルをどうするか聞かれるので、任意の方法で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を使って何かできないか試してみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftのenumはprotocolに準拠できるので、例えばComparableによってシンプルに比較できる

Swiftのenumは、protocolに準拠させることで、いろいろな振る舞いを与えることができます。
これは実務では強力な機能であり、ちょっとした工夫によってコードをシンプルにできたりします。

例えば以下のようなことができます。

  • Comparableに準拠することで、比較演算子を定義したり
  • CaseIterableに準拠することで、allCases プロパティを通して全ての値をコレクションとして取得できたり

コードサンプルで表現するとこんな感じです。

Playground
import 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutterを使ってクソアプリを作ってみた

これは何?

最近、若者の間で「クソアプリ」なるものを作って公開するのが話題になっているようで。いや確かに「ちゃんとしたもの」を作ろうとすると、それはそれで多大なる労力が必要になってハードルも上がるのだけれども、クソアプリでいいやってハードルを下げると「ちょっとかじってみる」程度の経験を積むにはいいし、やったことを公開することで情報の整理にもつながっていい気がするな、ということで最近気になるFlutterで負けじとクソアプリを作ってみた。

ソースコードはコチラ

Flutterとは?

Android/iOSのマルチデバイス対応のアプリが1つのソースコードで開発できるというGoogleが作った技術。Dart言語で実装する。

https://flutter.dev/

やったこと

開発環境の構築

大いにこの記事を参考にしており「開発環境の構築」は全く同じなので端折ります(手抜き)

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」を選択
    Kobito.OHndNY.png

    • Flutter Applicationを選択してNext
    • Project nameを適当に指定(デフォルトの flutter_app のままでもいい)してNext
    • Company domainを適当に指定、Sample Applicationの "generate sample content" にチェックしてFinish
    • サンプルコード付きのプロジェクトが作成される Kobito.W0QeXI.png
      • main.dart にメインロジックが記載されている
      • ちゃんとテストコード widget_test.dart も用意されているのが素敵 :star:
    • Emulatorを用意して起動
      • AVD Managerから端末のVMを用意して起動する
      • +ボタンを押すと画面に表示された数値が増えていくサンプルアプリらしい
    • サンプルアプリの画像は前述の記事にキャプチャがあるので、そっちを見てください(という手抜き)

アプリを実装する

クソアプリの境界線ってどこだろう?うっかりちゃんと作ってしまうとクソではなくなってしまうので、ここは慎重にクソアプリを作る必要がある。

今回は、macOSを始めとして Google Chrome や Evernote でも実装されている流行りの DarkMode を搭載すべく「ダークモードへの切り替え」をこのアプリに実装してみることにする。

漢は黙ってCI

とはいえロジックに手を出し始めるのではなく、眼の前に動くコードとテストコードがあるなら、まずCIを回さないとね。ということでCircleCIでのビルドを設定しておく。

  • 生成されたサンプルプロジェクトをそのままコミット&GitHubへプッシュする。

次に挙げるサイトを参考にすると、すでにFlutter用のビルドコンテナが用意されているのが分かるので、単純に .circleci/config.yml を追加するだけで、CircleCI上でビルドが動くようになる。作業ログ

circleci/config.yml
 version: 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 の内容をまるっとこのサンプルに書き換えて実行してみたら、ちゃんとスイッチのサンプルが表示された。

Kobito.zdsg1w.png

そうそう、それよそれ。
今回はタイトル付きのスイッチを使いたいので、この部分+ _onChanged2() 関数をパクればよろし。

main.dart
new 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.dart
Container(
  padding: EdgeInsets.all(40.0),
  child: SwitchListTile(
    value: _isDarkMode,
    onChanged: _changeDarkMode,
    title: Text('Dark mode', style: new TextStyle(fontWeight: FontWeight.bold)),
  ),
),

見た目はこんな感じに。

Kobito.JqiQUR.png

パーツを配置しただけで「Dark modeに切り替えられる感」が8割増しましたね。

いざ、DarkModeへ切り替え

残るは _changeDarkMode() 関数の実装だ。ということで試しに backgroundColor 指定だけblackで追加したら、文字は黒のままで画面全体が真っ黒になってしまったので、どうやら何か違うらしい。調べてみると Theme を切り替える必要がありそう。

Using Themes to share colors and font styles - Flutter

でもこの実装を真面目にやると面倒くさいなぁ、と調べ続けていたら、DynamicTheme というドンピシャなwidgetが公開されていたので、これを使おう。

pubspec.yaml のdependenciesに1行追加する。

pubspec.yaml
dependencies:
  ...(中略)
  dynamic_theme: ^1.0.0

main.dart の修正箇所はこんな感じ。(抜粋)

main.dart
import '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に!ヒャッホウ!! :night_with_stars: dark-mode-sample2.gif
    • うーん、、、冷静に見ると「だからどうした感」のあるクソアプリですね :star2:
    • 参考:作業ログ

感想

  • 簡単にスマホアプリが作れそう
    • 今回はアプリを最終ビルドするところまでやっていないけど、簡単な動作確認レベルならすぐイケる
  • dart はキャッチアップ楽ちん
    • エディタの補完も効けば何の苦もない
  • ソースコードの保存 → エミュレータへの自動反映、での動作確認が非常に快適
  • クソアプリ作ってみるの楽しい

GW後半に軽くプログラミングしてみたい方は、いかがでしょうか?

お粗末さまでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む