20190502のSwiftに関する記事は6件です。

ページ送り出来る詳細画面

ページ送り出来る詳細画面の実装方法のまとめです
詳細画面でページ送りしたら一覧画面のcellの位置も同期させるように実装します

前置き

一覧のAPIと詳細のAPIで別れているような場合は実現が難しい気がします
詳細画面は一覧のAPIから大まかな表示が出来るようにして、詳細画面で必要な追加情報は個別のAPIを呼んで表示させるような設計が望ましいでしょう

なお今回は記載しませんが、詳細画面のインタラクティブなトランジション(dismiss)を実装しておくと使いやすくなります

実装

詳細画面を管理するPageViewControllerを実装します

一覧画面のリストに更新があった時(Paginateなど)、更新メソッド(updatePlaylist)を呼び最新のデータを反映させます
詳細画面をページ送りで遷移させた時、delegateで一覧画面に通知することで一覧画面のcellの位置を同期させることが出来ます

細かい実装は省略します(コメント参照)

Sample.swift
// 画面に表示するデータ
public struct Sample {
    public let ~~~
}
SampleNavigatorViewController.swift
internal protocol SampleNavigatorDelegate: class {
    /// PageViewControllerの遷移が完了した時に呼ばれる
    func transitionedToSample(at index: Int)
}

// 詳細画面を管理するPageViewController
class SampleNavigatorViewController: UIPageViewController {

    fileprivate weak var navigatorDelegate: SampleNavigatorDelegate?
    fileprivate let pageDataSource: SampleNavigatorPagesDataSource!

    internal static func configuredWith(
        sample: Sample,
        initialPlaylist: [Sample]? = nil,
        navigatorDelegate: SampleNavigatorDelegate?) -> SampleNavigatorViewController {
        let vc = SampleNavigatorViewController(
            initialSample: sample,
            initialPlaylist: initialPlaylist,
            navigatorDelegate: navigatorDelegate
        )
        vc.setViewControllers(
            [.init()],
            direction: .forward,
            animated: true,
            completion: nil
        )
        return vc
    }

    private init(initialSample: Sample,
                 initialPlaylist: [Sample]?,
                 navigatorDelegate: SampleNavigatorDelegate?) {

        self.pageDataSource = SampleNavigatorPagesDataSource(initialPlaylist: initialPlaylist,
                                                             initialSample: initialSample)
        self.navigatorDelegate = navigatorDelegate

        super.init(transitionStyle: .scroll,
                   navigationOrientation: .horizontal,
                   options:[UIPageViewController.OptionsKey.interPageSpacing: 6])
    }

    internal required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.dataSource = self.pageDataSource
        self.delegate = self
        self.view.backgroundColor = .white

        self.setInitialPagerViewController()
    }

    /// 呼び出し元のViewControllerはリストが更新される度にこのメソッドを呼ぶ必要がある
    internal func updatePlaylist(_ playlist: [Sample]) {
        self.pageDataSource.updatePlaylist(playlist)
    }

    fileprivate func setInitialPagerViewController() {
        guard let navController = self.pageDataSource.initialController() else { return }
        self.setViewControllers([navController], direction: .forward, animated: false, completion: nil)
    }
}

extension SampleNavigatorViewController: UIPageViewControllerDelegate {

    internal func pageViewController(_ pageViewController: UIPageViewController,
                                     didFinishAnimating finished: Bool,
                                     previousViewControllers: [UIViewController],
                                     transitionCompleted completed: Bool) {
        // `completed` がtrueなら、
        // SampleNavigatorDelegateの transitionedToSample(at:) を呼び、
        // pageViewController(_:willTransitionTo:) で取得した `newIndex` を渡すように実装する
    }

    internal func pageViewController(
        _ pageViewController: UIPageViewController,
        willTransitionTo pendingViewControllers: [UIViewController]) {

        guard let nav = pendingViewControllers.first as? UINavigationController else { return }
        let newIndex = self.pageDataSource.indexFor(controller: nav)

        // `newIndex` を変数に保持するように実装する
    }
}

DataSourceクラスでページ(詳細画面)の取得・作成・削除を行います
pageViewController(_:viewControllerBefore)pageViewController(_:viewControllerAfter)で前後の画面を作成します

SampleNavigatorPagesDataSource
import UIKit

internal final class SampleNavigatorPagesDataSource: NSObject, UIPageViewControllerDataSource {

    fileprivate let initialSample: Sample
    fileprivate var playlist: [Sample] = []
    fileprivate var viewControllers: [UIViewController?] = []

    init(initialPlaylist: [Sample]?, initialSample: Sample) {
        self.initialSample = initialSample
        self.playlist = initialPlaylist ?? [initialSample]

        super.init()

        self.padControllers(toLength: self.playlist.count)
    }

    internal func updatePlaylist(_ playlist: [Sample]) {
        self.playlist = playlist
    }

    internal func initialController() -> UIViewController? {
        return self.playlist.index(of: self.initialSample).flatMap(self.controllerFor(index:))
    }

    internal func initialDetailController() -> SampleDetailViewController? {
        return self.playlist.index(of: self.initialSample).flatMap(self.sampleDetailControllerFor(index:))
    }

    internal func controllerFor(index: Int) -> UIViewController? {
        guard index >= 0 && index < self.playlist.count else { return nil }

        let sample = self.playlist[index]
        self.padControllers(toLength: index)

        self.viewControllers[index] = self.viewControllers[index]
            ?? self.createViewController(forSample: sample)
        return self.viewControllers[index]
    }

    internal func sampleDetailControllerFor(index: Int) -> SampleDetailViewController? {
        return self.controllerFor(index: index)
            .flatMap { $0 as? UINavigationController }
            .flatMap { $0.viewControllers.first as? SampleDetailViewController }
    }

    internal func indexFor(controller: UIViewController) -> Int? {
        return self.viewControllers.index { $0 == controller }
    }

    internal func sampleFor(controller: UIViewController) -> Sample? {
        return self.indexFor(controller: controller).map { self.playlist[$0] }
    }

    internal func pageViewController(_ pageViewController: UIPageViewController,
                                     viewControllerBefore viewController: UIViewController) -> UIViewController? {

        guard let pageIdx = self.viewControllers.index(where: { $0 == viewController }) else {
            fatalError("Couldn't find \(viewController) in \(self.viewControllers)")
        }

        let previousPageIdx = pageIdx - 1
        guard previousPageIdx >= 0 else {
            return nil
        }

        let sample = self.playlist[previousPageIdx]
        self.padControllers(toLength: previousPageIdx)
        self.viewControllers[previousPageIdx] = self.viewControllers[previousPageIdx]
            ?? self.createViewController(forSample: sample)

        self.clearViewControllersFarAway(fromIndex: previousPageIdx)

        return self.viewControllers[previousPageIdx]
    }

    internal func pageViewController(_ pageViewController: UIPageViewController,
                                     viewControllerAfter viewController: UIViewController) -> UIViewController? {

        guard let pageIdx = self.viewControllers.index(where: { $0 == viewController }) else {
            fatalError("Couldn't find \(viewController) in \(self.viewControllers)")
        }

        let nextPageIdx = pageIdx + 1
        guard nextPageIdx < self.playlist.count else {
            return nil
        }

        let sample = self.playlist[nextPageIdx]
        self.padControllers(toLength: nextPageIdx)
        self.viewControllers[nextPageIdx] = self.viewControllers[nextPageIdx]
            ?? self.createViewController(forSample: sample)

        self.clearViewControllersFarAway(fromIndex: nextPageIdx)

        return self.viewControllers[nextPageIdx]
    }

    fileprivate func createViewController(forSample: Sample) -> UIViewController {
        return UINavigationController(
            rootViewController: SampleDetailViewController.configuredWith(
                sample: forSample
            )
        )
    }

    fileprivate func clearViewControllersFarAway(fromIndex index: Int) {
        // 前後のページ以外は必要ないので削除する
        self.viewControllers.indices
            .filter { abs($0 - index) >= 3 }
            .forEach { idx in
                self.viewControllers[idx] = nil
        }
    }

    fileprivate func padControllers(toLength length: Int) {
        guard self.viewControllers.count <= length else { return }

        (self.viewControllers.count...length).forEach { _ in
            self.viewControllers.append(nil)
        }
    }
}

一覧画面
こちらも細かい実装は省略します(コメント参照)

class SamplesViewController: UITableViewController {
    // viewDidLoadなどの実装

    // tableView(_:didSelectRowAt)で呼ぶように実装する
    // tapしたSampleとリスト全体のSample配列を渡す
    private func goTo(sample: Sample, initialPlaylist: [Sample]) {
        let vc = SampleNavigatorViewController.configuredWith(sample: sample,
                                                              initialPlaylist: initialPlaylist,
                                                              navigatorDelegate: self)
        self.present(vc, animated: true, completion: nil)
    }

    // 一覧画面のリストに更新があった時(Paginateなど)、このメソッドを呼ぶように実装する
    private func updateSamplePlaylist(_ playlist: [Sample]) {
        guard let navigator = self.presentedViewController as? SampleNavigatorViewController else { return }
        navigator.updatePlaylist(playlist)
    }
}

extension SamplesViewController: SampleNavigatorDelegate {
    func transitionedToSample(at index: Int) {
        self.tableView.scrollToRow(at: self.dataSource.indexPath(forSampleRow: index),
                                   at: .top,
                                   animated: false)
    }
}
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【学習記録12】2019/5/2(木)

学習時間

3.0H

使用教材

絶対に挫折しないiPhoneアプリ開発「超」入門 第7版 【Xcode 10 & iOS 12】 完全対応 (Informatics&IDEA)

Progate リンク[https://prog-8.com/]

学習分野

Chapter09 今後につながる少し高度なアプリ開発
09-03:画面遷移の実装方法

Progate
Swift学習コースⅠ
 1.Swiftとは?
 2.Swiftの基礎
 3.条件分岐

コメント

学習開始からの期間:12日目
今日までの合計時間:33.0H

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