Functional Swift 4章 "Map Filter Reduce" まとめ

引数に関数を取る関数を高階関数と呼びます。本章ではSwiftの標準ライブラリに実装されている高階関数を紹介します。

Map, Generic

Int型の配列を受け取り、全ての要素に1を加えて返す関数はfor文を使って簡単に書くことができます。

func incrementArray(xs: [Int]) -> [Int] {
    var result: [Int] = []
    for x in xs {
        result.append(x + 1)
    }
    return result
}

全ての要素を2倍にする関数も同様に書くことができるでしょう。

しかし、これらの関数をより汎用性のあるものにするためには、配列の各要素をとって計算し、Int型を返す関数を第二引数として受けとる必要があります。

func computeIntArray(xs: [Int], transform: Int -> Int) -> [Int] {
    var result: [Int] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}

しかし、このコードはまだ最も柔軟であるとは言えません。 例えば、配列の各要素を偶数であるか判定し、真偽値の値を配列に入れて返したい場合はどうすれば良いでしょうか。

この問題を解決してくれるのがジェネリクスです。

func genericComputeArray<T>(xs: [Int], transform: Int -> T) -> [T] {
    var result: [T] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}

これで任意の型で動作する関数を書くことができました。

この関数はさらに抽象化することができます。入力される配列は[Int]である必要はありません。

func map<Element, T>(xs: [Element], transform: Element -> T) -> [T] {
    var result: [T] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}

グローバルレベルでmap関数を定義するのではなく、Arrayの拡張として定義することでSwiftにうまくフィットします。

extension Array {
    func map<T>(transform: Element -> T) -> [T] {
        var result: [T] = []
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}

map関数はSwiftの標準ライブラリに含まれているので自分で実装する必要はありませんが、map関数はやろうと思えば簡単に実装できることは示したのです。

Filter

あるディレクトリにこんなファイルがあるとします。

let exampleFiles = ["README.md", "HelloWorld.swift", "FlappyBird.swift"]

この中からSwiftファイルだけを取り出したい場合、シンプルな配列で書くことができます。

func getSwiftFiles(files: [String]) -> [String] {
    var result: [String] = []
    for file in files {
        if file.hasSuffix(".swift") {
            result.append(file)
        }
    }
    return result
}

これを抽象化するとFilter関数ができます。

extension Array {
    func filter(includElement: Element -> Bool) -> [Element] {
        var result: [Element] = []
        for x in self where includElement(x) {
            result.append(x)
        }
        return result
    }
}

Reduce

配列内の合計を計算する関数を作るのは簡単です。

func sum(xs: [Int]) -> Int {
    var result: Int = 0
    for x in xs {
        result += x
    }
    return result
}

抽象化してReduce関数ができます。

extension Array {
    func reduce<T>(initial: T, combine: (T, Element) -> T) -> T {
        var result = initial
        for x in self {
            result = combine(result, x)
        }
        return result
    }
}

Putting It All Together

締めとして、map, filter, reduceを使ったささやかな例を紹介します。

都市名と人口を持つ構造体があります。

struct City {
    let name: String
    let population: Int
}

都市をいくつか定義します。

let paris = City(name: "Paris", population: 2241)
let madrid = City(name: "Madrid", population: 3165)
let amsterdam = City(name: "Amsterdam", population: 827)
let berlin = City(name: "Berlin", population: 3562)

let cities = [paris, madrid, amsterdam, berlin]

ここで、100万人以上の人口を持つ都市を人口と一緒に出力したいと思います。

まず、人口の単位を返還するヘルパー関数を定義します。

extension City {
    func cityByScalingPopulation() -> City {
        return City(name: name, population: population * 1000)
    }
}

ここで本章で紹介したすべてのパーツを使って、次のようなコードが書けます。

cities.filter { $0.population > 1000 }
    .map { $0.cityByScalingPopulation() }
    .reduce("") { result, c in
        result + "\n" + "\(c.name): \(c.population)"
    }
Paris: 2241000
Madrid: 3165000
Berlin: 3562000

Swift標準ライブラリの持つmap, filter, reduceをうまくチェーンできました。

Functional Swift 3章 "Wrapping Core Image" まとめ

この章ではCore ImageをfunctionalにラッピングするAPIを構築することで、より実戦的に高階関数と関数合成を利用する方法を学びます。

Filter型

typealias Filter = CIImage -> CIImage

オリジナルの let filter = CIFilter(name: "CIVignette") などのkey値で初期化するCIFilterをカプセル化するためにFilter型を関数として定義。

フィルタの構築

Filter型を定義したので、個別のFilterを定義していく

全て func myFilter("パラメータ") -> Filter の形

Blur

func blur(radius: Double) -> Filter {
        return { image in
            let parameters = [
                kCIInputRadiusKey: radius,
                kCIInputImageKey: image
            ]
            guard let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: parameters) else { fatalError() }
            guard let outputImage = filter.outputImage else { fatalError() }
            return outputImage
        }
    }

引数はぼかしの半径のみです。 CIImage型のimageをとり、新しいCIImageを返す関数(先ほど定義したFilter(CIImage -> CIImage))を返します。 同じパターンで様々なフィルタ関数を定義できます。

Chrome

func chrome() -> Filter {
        return { image in
            let parameters = [
                kCIInputImageKey: image
            ]
            guard let filter = CIFilter(name: "CIPhotoEffectChrome", withInputParameters: parameters) else { fatalError() }
            guard let outputImage = filter.outputImage else { fatalError() }
            return outputImage
        }
    }

*1

フィルタを合成

ここまで作成したぼかしとクロームのフィルタ関数を合成していきます。

let url = NSURL(string: "http://www.objc.io/images/covers/16.jpg")
let image = CIImage(contentsOfURL: url!)!
        
let blurredImage = blur(5.0)(image)
let chromedImage = chrome()(blurredImage)

一度ブラーをかけたimageに新たにクロームフィルタをかけて画像を生成しています。 もちろん一つにまとめることもできますが、括弧が増え途端に読めなくなってしまいます。

let result = chrome()(blur(5.0)(image))

そこで、カスタムオペレーションを定義することで読みやすさを保つことができます。

infix operator >>> { associativity left }

func >>>(filter1: Filter, filter2: Filter) -> Filter {
    return { image in filter2(filter1(image)) }
}


let filter = chrome() >>> blur(5.0)
let result = filter(image)

高階関数、関数合成を使用することによって、安全性、モジュール性、明瞭性を保ったAPIをデザインすることができます。

*1:本書では別のフィルタ関数を使っていますが、複雑になるので引数なしのChromeに変更

SwiftBondでAPIからデータバインディング

データバインディングのライブラリとしてBondを初めてつかってみました。

github.com

いろいろやってみるうちに、MVVM風にViewModelからデータをバインディングする設計が良い感じで気に入りました

サンプルコードはgithubを参照

github.com

実装

iTuens search apiを使って、アーティストと曲名をTableViewに表示します

Model

import SwiftyJSON

struct List {

    let trackName: String
    let artistName: String
    
    init(json: JSON) {
        self.trackName = json["trackName"].stringValue
        self.artistName = json["artistName"].stringValue
    }

}

View Model

モデルをObservableArrayで保持し、apiコールで更新します。

import UIKit
import Bond
import Alamofire
import SwiftyJSON

class ListViewModel {

    internal let lists = ObservableArray<List>()
    
    private let urlString = "https://itunes.apple.com/search"
    private let parameters = ["term":"Swift",
                              "entity":"musicTrack",
                              "limit":"15"]

    internal func reload() {
        Alamofire.request(.GET, urlString, parameters: parameters).validate().responseJSON { response in
            switch response.result {
            case .Success:
                if let value = response.result.value {
                    let jsons = JSON(value)
                    for i in 0...jsons["results"].count {
                        let list = List(json: jsons["results"][i])
                        self.lists.append(list)
                    }
                }
            case .Failure(let error):
                print(error)
            }
        }
    }
    
}

View Controller

ObservableArray<List>をtableViewにBindします

import UIKit
import Bond

class ListViewController: UIViewController {
    
    @IBOutlet var tableView: UITableView!

    private let listViewModel = ListViewModel()
    private var list = ObservableArray<ObservableArray<List>>()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.list = [listViewModel.lists]
        listViewModel.reload()
        
        list.bindTo(tableView) { indexPath, dataSources, tableView in
            let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
            let dataSource = dataSources[indexPath.section][indexPath.row]
            cell.textLabel?.text = dataSource.trackName
            cell.detailTextLabel?.text = dataSource.artistName

            return cell
        }

    }

}

f:id:aminaura:20160604154910p:plain

SwiftでアプリからTwitterにGif投稿

アプリで撮影した動画をgifに変換し、twitterに投稿してみた。

TwitterGifをサポートしてるけど、既存のSLComposeViewControllerや、UIActivityViewControllergifをサポートしていないので少し面倒でした。

動画をgifに変換

Regiftというライブラリを使ったら簡単にできた

github.com

twitterに投稿

まずはActionSheetで全アカウントを表示し、選択されたaccountを返します

import Accounts

func selectAccount() -> AnyObject {
            let account = ACAccountStore()
            let accountType = account.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)
            account.requestAccessToAccountsWithType(accountType, options: nil, completion: { (success: Bool, error: NSError!) -> Void in
                if success {
                    guard let accounts = account.accountsWithAccountType(accountType) else { return }
                    if !accounts.isEmpty {
                        let actionSheet = UIAlertController(title: "Choose Account", message: "", preferredStyle: .ActionSheet)
                        for account in accounts {
                            guard let name = account.username else { return }
                            let action = UIAlertAction(title: name, style: .Default, handler: { (action: UIAlertAction) in
                                return account
                            })
                            actionSheet.addAction(action)
                        }
                        self.presentViewController(actionSheet, animated: true, completion: nil)
                    }
                }
            })
        }
    }

選択されたアカウント、メッセージ、gifのNSDataでポスト。 メッセージはDictionaryで["status", ツイート内容]

func post(account: ACAccount, message: [String: String!], data: NSData) {
        let url = NSURL(string: "https://api.twitter.com/1.1/statuses/update_with_media.json")
        let postRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: url, parameters: message)
        postRequest.account = account
        postRequest.addMultipartData(data, withName: "media", type: "image/gif", filename: "image.gif")
        postRequest.performRequestWithHandler({ (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
            print(urlResponse)
        })
    }

SwiftでAWS S3へデータをアップロード

AWS SDK for iOSを使ってアプリからAWS S3に動画をアップロードする処理をやってみた。 ローカルのデータをアップロードして、保存先のURLを取得します。

準備

  • プロジェクトにAWS SDKをインストール

pod 'AWSS3'

  • ユーザー認証の仕組みとかを実装できる Amazon CognitoというAWSサービスを登録する必要もあります。

  • S3の設定 Bucketを作成します。 permissionはとりあえず、EveryOne が UploadとRead できるようにしておきます。

これで準備完了。

実装

application:didFinishLaunchingWithOptionsでCognitoを使ってSDKのセットアップ

let credentialsProvider = AWSCognitoCredentialsProvider(regionType: RegionType, identityPoolId: IdentityPoolId)
let configuration = AWSServiceConfiguration(region: DefaultServiceRegionType, credentialsProvider: credentialsProvider)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration

実際にアップロードするところは

internal func upload() -> NSURL {
        let uploadRequest = AWSS3TransferManagerUploadRequest()
        uploadRequest.body = origin
        uploadRequest.key = remote
        uploadRequest.bucket = BucketName
        let transferManager = AWSS3TransferManager.defaultS3TransferManager()
        transferManager.upload(uploadRequest).continueWithBlock { task -> AnyObject! in
            if task.result != nil {
                if let bucket = uploadRequest.bucket, key = uploadRequest.key {
                    let url = NSURL(string: "http://s3.amazonaws.com/\(bucket)/\(key)")
                    return url
                }
            }
        }
    }

originにアップロードするデータのfilepath、remoteにS3に保存するデータの名前を入れます。 アップロードが成功すると保存先のURLが返ってきます。

使いやすいように、SwiftTaskを使ってこんな感じのクラスを作ってみた

import AWSS3
import SwiftTask

class S3Uploader {

    typealias UploadTask = Task<Void, NSURL, NSError>

    private var origin: NSURL?
    private var remote: String?

    internal init(origin: NSURL, remote: String) {
        self.origin = origin
        self.remote = remote
    }

    internal func upload() -> UploadTask {
        return UploadTask { progress, fulfill, reject, configure in
            guard let origin = self.origin, remote = self.remote else { return }
            let uploadRequest = AWSS3TransferManagerUploadRequest()
            uploadRequest.body = origin
            uploadRequest.key = remote
            uploadRequest.bucket = AppConfig.AWSS3.BucketName
            let transferManager = AWSS3TransferManager.defaultS3TransferManager()
            transferManager.upload(uploadRequest).continueWithBlock { task -> AnyObject! in
                if let error = task.error {
                    reject(error)
                }
                if task.result != nil {
                    if let bucket = uploadRequest.bucket, key = uploadRequest.key {
                        if let url = NSURL(string: "http://s3.amazonaws.com/\(bucket)/\(key)") {
                            fulfill(url)
                        }
                    }
                }
                return nil
            }
        }
    }
}

けっこう頻繁に更新されている公式のサンプルもあって、参考になった。(日本人の方がつくってるっぽい)

github.com

SwiftでLINEの桜が降るエフェクトを作ってみた

これです。

ほんとはLINEに実装される前にCocoaPodsになにか公開しようと思って作ったものなんだけど...

CoreAnimationのパーティクルシステム、CAEmitterLayerCAEmmiterCellを使ってけっこう簡単にできました。

github.com

f:id:aminaura:20160504173612p:plain

桜の他にも、タンポポとPlumも選べるようにした

SwiftでSnapChatっぽいUI

SwiftでSnapChatぽいUI、左右のスワイプでViewControllerが切り替わるやつをやってみた。

f:id:aminaura:20160503073406j:plain

まずは大元となるContainerViewControllerを作って(Containerという名前が正しいかはわからないけど)、その上に敷いたScrollViewにVCたちをaddChildViewControllerしていく感じ。

class ContainerViewController: UIViewController {

    private var offset = 0
    private var viewControllers = [UIViewController]()

    internal init(controllers: [UIViewController], offset: Int) {
        super.init(nibName: nil, bundle: nil)
        controllers.forEach { viewControllers.append($0) }
        self.offset = offset
    }

    private func setupScrollView() {
        let viewBounds = self.view.bounds
        let viewWidth = viewBounds.width

        let scrollView = UIScrollView()
        scrollView.delegate = self
        scrollView.pagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.bounces = false
        scrollView.frame = CGRect(x: viewBounds.origin.x, y: viewBounds.origin.y, width: viewWidth, height: CGRectGetHeight(viewBounds))

        self.view.addSubview(scrollView)
        scrollView.contentSize = CGSize(width: CGFloat(viewControllers.count) * viewWidth, height: CGRectGetHeight(viewBounds))

        for (index, vc) in viewControllers.enumerate() {
            vc.view.frame = CGRect(x: CGFloat(index)*viewWidth, y: 0, width: viewWidth, height: CGRectGetHeight(viewBounds))
            self.addChildViewController(vc)
            scrollView.addSubview(vc.view)
            vc.didMoveToParentViewController(self)
            scrollView.sendSubviewToBack(vc.view)
        }
        scrollView.contentOffset.x = viewControllers[offset].view.frame.origin.x
    }
}

ContainerViewControllerの初期化はアプリ起動時にこんな感じで行ってる。

internal func setup(application: UIApplication) {
        let cameraSB = UIStoryboard(name: "Camera", bundle: nil)
        let feedSB = UIStoryboard(name: "Feed", bundle: nil)
        let profileSB = UIStoryboard(name: "Profile", bundle: nil)

        let camera = cameraSB.instantiateViewControllerWithIdentifier("camera")
        let feed = feedSB.instantiateViewControllerWithIdentifier("feed")
        let profile = profileSB.instantiateViewControllerWithIdentifier("profile")
        let controllers = [feed, camera, profile]

        let container = ContainerViewController(controllers: controllers, offset: 1)
        self.window?.rootViewController = container
        self.window?.makeKeyAndVisible()
    }

(実際のコードから削っているのでそのままだと動かないかもしれないです)