【2018年版】iOSアプリ新規で作る時にやること③

これ年に10回くらいはやるんだけど、どうにかならんのかなぁ。シェルスクリプト書けばいけそうだけど、Xcodeについてくのだるいし、労力と比較して迷う。

Carthage / RxSwift / Compass / XCGLogger / Reachability / Alamofire / R.swift / SwiftLint / Generamba / VIPER / fastlaneあたりが技術キーワードでしょうか。箇条書きでいきまっせ。
ぼくはもうCarthage対応してないやつは使わないので、Cocoapodsは出てきません。


③CD編です。fastlaneが主。
CIは対応してません。
あとたぶん設定抜けてたけど、AppIconは設定しておきましょう。
↑に挙げたやつ残はRxSwift / Compass / XCGLogger / Reachability / Alamofire / VIPERとまぁ実装寄りだし、このシリーズは今回で終わりかなぁ。

①はこちら
devdevdev.hatenablog.com

②はこちら
devdevdev.hatenablog.com

とりあえず

Xcode開いて、TARGETS→General→Signingで「Automatically manage signing」を外しておきましょう。

fastlane設定など

順番通りにやってくればfastlaneが使えるようになっているはずなのでインストールは省略。

bundle exec fastlane init

しましょう。あとは質問に答えていくだけ。
とりあえず2. Automate beta distribution to TestFlightやったあとに、3.Automate App Store distributionをやりました。Schemaが2つあるので2つともね。ただ、StagingのApp Store distributionはいらないので計3回。

・・・・・・という風にはうまくいきません。実際にはやっていません。


そして、Generambaとfastlaneの相性がよくない。ってゆーかGenerambaが見てるxcodeprojが古すぎ。本家Generamba更新なさすぎ。つらたん。自作?いや、めんどい。Fork?いや、誰かやってるっしょってわけで以下の方を発見。Xcode9.4で動いてるっぽいことをぼくが確認しました。これでもxcodeproj 1.5.9だからぎり。。fastlaneはxcodeproj 1.5.7以上。。fastlaneをグローバルにいれればいける気がするけど、でもグローバルにはいれたくない。

github.com

とゆーわけでGemfileを更新。

source "https://rubygems.org"
  
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'generamba', github: "Bassov/Generamba", branch: "develop"
gem "fastlane", "2.99.0"

bundle updateとかbundle installとかしましょう。



んで、本題。

とりあえず証明書管理用のgitリポジトリを作りましょう。

で、devices.txtを作成。
バイス追加するときはこいつを更新しましょう。

Device ID	Device Name
A5B5CD50-14AB-5AF7-8B78-AB4751AB10A8	NAME1
A5B5CD50-14AB-5AF7-8B78-AB4751AB10A7	NAME2

fastlane/Appfileとfastlane/Fastfileを作成。
Fastfileは手抜き。ちゃんと書きましょう。

Appfile

itc_team_id("123456789") # iTunes Connect Team ID
team_id("ABCDEFGHIJ") # Developer Portal Team ID

Fastfile

default_platform(:ios)

platform :ios do
  desc "Get the development certifacate files to Local Machine"
  lane :get_development_cert do
    match(type: "development",
          readonly: true,
          git_url: "証明書管理用のgitリポジトリURL",
          app_identifier: ["本番用Bundle Identifier", "ステージング用Bundle Identifier"])
  end

  desc "Push a new Staging beta build to TestFlight"
  lane :staging_beta do
    beta(
      app_identifier: "ステージング用Bundle Identifier",
      scheme: "Hoge_Staging"
    )
  end

  desc "Push a new Production beta build to TestFlight"
  lane :production_beta do
    beta(
      app_identifier: "本番用Bundle Identifier",
      scheme: "Hoge_Production"
    )
  end

  private_lane :beta do |options|
    # テスト
    # run_tests(scheme: options[:scheme])

    # デバイス更新
    register_devices(devices_file: "./devices.txt")

    # 証明書更新
    match(type: "appstore",
          force_for_new_devices: true,
          git_url: "証明書管理用のgitリポジトリURL",
          app_identifier: ["本番用Bundle Identifier", "ステージング用Bundle Identifier"])

    # ビルド番号更新
    increment_build_number

    # ビルド
    build_app(scheme: options[:scheme])

    # Testflightにアップロード
    upload_to_testflight(skip_waiting_for_build_processing: true,
                         app_identifier: options[:app_identifier])
  end

  private_lane :refresh_development_cert do
    match(type: "development",
          force: true,
          git_url: "証明書管理用のgitリポジトリURL",
          app_identifier: ["本番用Bundle Identifier", "ステージング用Bundle Identifier"])
  end
end



refresh_development_cert

bundle exec fastlane refresh_development_cert

開発用の証明書の更新をしまっせ。
なんかあんま頻繁に更新されたくない気がしたので、private。使うときは書き換え。運用でカバー)

get_development_cert

bundle exec fastlane get_development_cert

一般エンジニアが使う用。
初めてrefresh_development_certした時にパスワード設定するので、それを共有しといてあげましょう。

staging_beta

bundle exec fastlane staging_beta

ステージング用のアプリをTestflightへ
バイスとProvisioning Profileの更新(必要なら)もやってます。

production_beta

bundle exec fastlane production_beta

本番用のアプリをTestflightへ
バイスとProvisioning Profileの更新(必要なら)もやってます。

余談

ちなみにぼくはrswiftがArchive対象に入っちゃってたらしく最初エラーになったぉ。
最近のXcodeってほんと親切だよね。
何回もApple IDいれんのめんどくさいよね。user_nameっていう設定がたぶんAppfileにできるよ。



最後に

Xcode開いて、TARGETS→General→Signingで選べるやつ選んでおきましょう。

【Extension晒すシリーズ】UIView



import UIKit

extension UIView {

    public typealias Completion = () -> Void

    /// SubViewの一番右の座標取得
    public var maxSubViewRight: CGFloat {
        guard let view = subviews.max(by: { (a, b) in
            return a.frame.origin.x + a.frame.width <= b.frame.origin.x + b.frame.width
        }) else {
            return 0
        }
        return view.frame.origin.x + view.frame.width
    }

    /// SubViewの一番下の座標取得
    public var maxSubViewBottom: CGFloat {
        guard let view = subviews.max(by: { (a, b) -> Bool in
            return a.frame.origin.y + a.frame.height < b.frame.origin.y + b.frame.height
        }) else {
            return 0
        }
        return view.frame.origin.y + view.frame.height
    }

    /// SubViewの最大サイズ
    public var maxSubViewSize: CGSize {
        return CGSize(width: maxSubViewRight, height: maxSubViewBottom)
    }

    /// 角Radius
    @IBInspectable public var cornerRadius: CGFloat {
        get {
            return layer.cornerRadius
        }
        set {
            layer.cornerRadius = newValue
            layer.masksToBounds = newValue > 0
        }
    }

    /// 枠線太さ
    @IBInspectable public var borderWidth: CGFloat {
        get {
            return self.layer.borderWidth
        }
        set {
            self.layer.borderWidth = newValue
        }
    }

    /// 枠線色
    @IBInspectable public var borderColor: UIColor? {
        get {
            return UIColor.init(cgColor: self.layer.borderColor!)
        }
        set {
            self.layer.borderColor = newValue?.cgColor
        }
    }

    /// Nibを生成
    ///
    /// - Returns: Nib
    public class func createNib() -> UINib {
        return UINib(nibName: className, bundle: Bundle(for: self))
    }

    /// Nibからロード
    /// - returns: インスタンス
    open class func loadFromNib() -> UIView {
        return createNib().instantiate(withOwner: self, options: nil)[0] as! UIView
    }

    /// Marginを指定してaddSubview
    /// - parameter view:   view
    /// - parameter top:    top margin
    /// - parameter bottom: bottom margin
    /// - parameter left:   left margin
    /// - parameter right:  right margin
    public func addSubviewWithFit(_ view: UIView, top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
        addSubview(view)
        view.fitToParent(top: top, bottom: bottom, left: left, right: right)
    }

    /// 親にfitさせる
    /// - parameter view:   view
    /// - parameter top:    top margin
    /// - parameter bottom: bottom margin
    /// - parameter left:   left margin
    /// - parameter right:  right margin
    public func fitToParent(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
        guard let superview = superview else {
            return
        }
        bounds = superview.bounds
        translatesAutoresizingMaskIntoConstraints = false

        superview.addConstraints([
            NSLayoutConstraint(
                item: self,
                attribute: .left,
                relatedBy: .equal,
                toItem: superview,
                attribute: .left,
                multiplier: 1.0,
                constant: left
            ),

            NSLayoutConstraint(
                item: self,
                attribute: .top,
                relatedBy: .equal,
                toItem: superview,
                attribute: .top,
                multiplier: 1.0,
                constant: top
            ),

            NSLayoutConstraint(
                item: self,
                attribute: .right,
                relatedBy: .equal,
                toItem: superview,
                attribute: .right,
                multiplier: 1.0,
                constant: right
            ),

            NSLayoutConstraint(
                item: self,
                attribute: .bottom,
                relatedBy: .equal,
                toItem: superview,
                attribute: .bottom,
                multiplier: 1.0,
                constant: bottom
            )
            ]
        )
    }

    /// centerにaddSubView
    ///
    /// - Parameter view: view
    public func addSubViewToCenter(_ view: UIView) {
        addSubview(view)
        view.moveToParentCenter()
    }

    /// 親のcenterに配置する
    public func moveToParentCenter() {
        guard let superview = superview else {
            return
        }
        bounds = superview.bounds
        translatesAutoresizingMaskIntoConstraints = false

        superview.addConstraints([
            NSLayoutConstraint(
                item: self,
                attribute: .centerX,
                relatedBy: .equal,
                toItem: superview,
                attribute: .centerX,
                multiplier: 1.0,
                constant: 1
            ),
            NSLayoutConstraint(
                item: self,
                attribute: .centerY,
                relatedBy: .equal,
                toItem: superview,
                attribute: .centerY,
                multiplier: 1.0,
                constant: 1
            )
            ]
        )
    }

    /// フェードイン
    ///
    /// - Parameters:
    ///   - duration: 時間
    ///   - completion: 完了通知
    public func fadeIn(duration: TimeInterval, completion: Completion?) {
        fadeImpl(true, duration: duration, completion: completion)
    }

    /// フェードアウト
    ///
    /// - Parameters:
    ///   - duration: 時間
    ///   - completion: 完了通知
    public func fadeOut(duration: TimeInterval, completion: Completion?) {
        fadeImpl(false, duration: duration, completion: completion)
    }

    private func fadeImpl(_ isfadeIn: Bool, duration: TimeInterval, completion: Completion?) {
        alpha = isfadeIn ? 0 : 1
        self.layer.removeAllAnimations()
        UIView.animate(withDuration: duration, animations: {
            self.alpha = isfadeIn ? 1 : 0
        }, completion: { finished in
            if finished {
                completion?()
            }
        })
    }

    /// 子を全て削除
    public func removeAllSubView() {
        subviews.forEach { (subView) in
            subView.removeFromSuperview()
        }
    }
}

【Swift】HTTPステータスコード列挙型 Powered by Wikipedia

devdevdev.hatenablog.com
↑これの更新版。4年ぶり。
Swiftで書き直しました。なんかQiitaで書いた気がするんだけどねー。akippaのやつかなー。



/// HTTPステータスコード列挙型
///
/// - cContinue: 継続 クライアントはリクエストを継続できる。
/// - switchingProtocols: プロトコル切り替え サーバはリクエストを理解し、遂行のためにプロトコルの切り替えを要求している
/// - processing: 処理中 WebDAVの拡張ステータスコード。処理が継続して行われていることを示す。
/// - ok: OK リクエストは成功し、レスポンスとともに要求に応じた情報が返される。
/// - created: 作成 リクエストは完了し、新たに作成されたリソースのURIが返される。
/// - accepted: 受理 リクエストは受理されたが、処理は完了していない。
/// - nonAuthoritativeInformation: 信頼できない情報 オリジナルのデータではなく、ローカルやプロキシ等からの情報であることを示す。
/// - noContent: 内容なし リクエストを受理したが、返すべきレスポンスエンティティが存在しない場合に返される。
/// - resetContent: 内容のリセット リクエストを受理し、ユーザエージェントの画面をリセットする場合に返される。
/// - partialContent: 部分的内容 部分的GETリクエストを受理したときに、返される。
/// - multiStatus: 複数のステータス WebDAVの拡張ステータスコード。
/// - IMUsed: IM使用 Delta encoding in HTTPの拡張ステータスコード。
/// - multipleChoices: 複数の選択 リクエストしたリソースが複数存在し、ユーザやユーザーエージェントに選択肢を提示するときに返される。
/// - movedPermanently: 恒久的に移動した リクエストしたリソースが恒久的に移動されているときに返される。Location:ヘッダに移動先のURLが示されている。
/// - found: 発見した リクエストしたリソースが一時的に移動されているときに返される。Location:ヘッダに移動先のURLが示されている。
/// - seeOther: 他を参照せよ リクエストに対するレスポンスが他のURLに存在するときに返される。Location:ヘッダに移動先のURLが示されている。
/// - notModified: 未更新 リクエストしたリソースは更新されていないことを示す。
/// - useProxy: プロキシを使用せよ レスポンスのLocation:ヘッダに示されるプロキシを使用してリクエストを行わなければならないことを示す。
/// - unUsed: 将来のために予約されている。ステータスコードは前のバージョンの仕様書では使われていたが、もはや使われておらず、将来のために予約されているとされる。
/// - temporaryRedirect: 一時的リダイレクト リクエストしたリソースは一時的に移動されているときに返される。Location:ヘッダに移動先のURLが示されている。
/// - badRequest: リクエストが不正である 定義されていないメソッドを使うなど、クライアントのリクエストがおかしい場合に返される。
/// - unauthorized: 認証が必要である Basic認証やDigest認証などを行うときに使用される。
/// - paymentRequired: 支払いが必要である 現在は実装されておらず、将来のために予約されているとされる。
/// - forbidden: 禁止されている リソースにアクセスすることを拒否された。
/// - notFound: 未検出 リソースが見つからなかった。
/// - methodNotAllowed: 許可されていないメソッド 許可されていないメソッドを使用しようとした。
/// - notAcceptable: 受理できない Accept関連のヘッダに受理できない内容が含まれている場合に返される。
/// - proxyAuthenticationRequired: プロキシ認証が必要である プロキシの認証が必要な場合に返される。
/// - requestTimeout: リクエストタイムアウト リクエストが時間以内に完了していない場合に返される。
/// - conflict: 矛盾 要求は現在のリソースと矛盾するので完了できない。
/// - gone: 消滅した。ファイルは恒久的に移動した。
/// - lengthRequired: 長さが必要 Content-Lengthヘッダがないのでサーバーがアクセスを拒否した場合に返される。
/// - preconditionFailed: 前提条件で失敗した 前提条件が偽だった場合に返される。
/// - requestEntityTooLarge: リクエストエンティティが大きすぎる リクエストエンティティがサーバの許容範囲を超えている場合に返す。
/// - requestURITooLong: リクエストURIが大きすぎる URIが長過ぎるのでサーバが処理を拒否した場合に返す。
/// - unsupportedMediaType: サポートしていないメディアタイプ 指定されたメディアタイプがサーバでサポートされていない場合に返す。
/// - requestedRangeNotSatisfiable: リクエストしたレンジは範囲外にある 実ファイルのサイズを超えるデータを要求した。
/// - expectationFailed: Expectヘッダによる拡張が失敗 その拡張はレスポンスできない。またはプロキシサーバは、次に到達するサーバがレスポンスできないと判断している。
/// - imaTeapot: 私はティーポット HTCPCP/1.0の拡張ステータスコード。
/// - unprocessableEntity: 処理できないエンティティ WebDAVの拡張ステータスコード。
/// - locked: ロックされている WebDAVの拡張ステータスコード。リクエストしたリソースがロックされている場合に返す。
/// - failedDependency: 依存関係で失敗 WebDAVの拡張ステータスコード。
/// - upgradeRequired: アップグレード要求 Upgrading to TLS Within HTTP/1.1の拡張ステータスコード。
/// - internalServerError: サーバ内部エラー サーバ内部にエラーが発生した場合に返される。
/// - notImplemented: 実装されていない 実装されていないメソッドを使用した。
/// - badGateway: 不正なゲートウェイ ゲートウェイ・プロキシサーバは不正な要求を受け取り、これを拒否した。
/// - serviceUnavailable: サービス利用不可 サービスが一時的に過負荷やメンテナンスで使用不可能である。
/// - gatewayTimeout: ゲートウェイタイムアウト ゲートウェイ・プロキシサーバはURIから推測されるサーバからの適切なレスポンスがなくタイムアウトした。
/// - httpVersionNotSupported: サポートしていないHTTPバージョン リクエストがサポートされていないHTTPバージョンである場合に返される。
/// - variantAlsoNegotiates: Transparent Content Negotiation in HTTPで定義されている拡張ステータスコード。
/// - insufficientStorage: 容量不足 WebDAVの拡張ステータスコード。リクエストを処理するために必要なストレージの容量が足りない場合に返される。
/// - bandwidthLimitExceeded: 帯域幅制限超過 そのサーバに設定されている帯域幅(転送量)を使い切った場合に返される。
/// - notExtended: 拡張できない An HTTP Extension Frameworkで定義されている拡張ステータスコード。
public enum HttpStatusCode: Int {

    case cContinue = 100

    case switchingProtocols = 101

    case processing = 102

    case ok = 200

    case created = 201

    case accepted = 202

    case nonAuthoritativeInformation = 203

    case noContent = 204

    case resetContent = 205

    case partialContent = 206

    case multiStatus = 207

    case IMUsed = 226

    case multipleChoices = 300

    case movedPermanently = 301

    case found = 302

    case seeOther = 303

    case notModified = 304

    case useProxy = 305

    case unUsed = 306

    case temporaryRedirect = 307

    case badRequest = 400

    case unauthorized = 401

    case paymentRequired = 402

    case forbidden = 403

    case notFound = 404

    case methodNotAllowed = 405

    case notAcceptable = 406

    case proxyAuthenticationRequired = 407

    case requestTimeout = 408

    case conflict = 409

    case gone = 410

    case lengthRequired = 411

    case preconditionFailed = 412

    case requestEntityTooLarge = 413

    case requestURITooLong = 414

    case unsupportedMediaType = 415

    case requestedRangeNotSatisfiable = 416

    case expectationFailed = 417

    case imaTeapot = 418

    case unprocessableEntity = 422

    case locked = 423

    case failedDependency = 424

    case upgradeRequired = 426

    case internalServerError = 500

    case notImplemented = 501

    case badGateway = 502

    case serviceUnavailable = 503

    case gatewayTimeout = 504

    case httpVersionNotSupported = 505

    case variantAlsoNegotiates = 506

    case insufficientStorage = 507

    case bandwidthLimitExceeded = 509

    case notExtended = 510
}


【iOS】プラットフォーム情報取得4~Swift編~【2018年版】

devdevdev.hatenablog.com
↑3年半ぶりに更新。Swiftで書き直したのとiPhoneX / iPad Proまで入ってるのと、ちょこっと機能追加!



import UIKit

public struct Device {

    static let iPhoneX = "iPhone X"
    static let iPad = "iPad"
    static let iPodTouch = "iPod Touch"
    static let iPhone = "iPhone"

    static var deviceInfo: String {
        var size: Int = 0
        sysctlbyname("hw.machine", nil, &size, nil, 0)
        var machine = [CChar](repeating: 0, count: Int(size))
        sysctlbyname("hw.machine", &machine, &size, nil, 0)
        let code: String = String(cString: machine)

        let deviceCodeDic: [String: String] = [
            /* Simulator */
            "i386": "Simulator",
            "x86_64": "Simulator",
            /* iPod */
            "iPod1,1": "iPod Touch 1th",            // iPod Touch 1th Generation
            "iPod2,1": "iPod Touch 2th",            // iPod Touch 2th Generation
            "iPod3,1": "iPod Touch 3th",            // iPod Touch 3th Generation
            "iPod4,1": "iPod Touch 4th",            // iPod Touch 4th Generation
            "iPod5,1": "iPod Touch 5th",            // iPod Touch 5th Generation
            "iPod7,1": "iPod Touch 6th",            // iPod Touch 6th Generation
            /* iPhone */
            "iPhone1,1": "iPhone 2G",                 // iPhone 2G
            "iPhone1,2": "iPhone 3G",                 // iPhone 3G
            "iPhone2,1": "iPhone 3GS",                // iPhone 3GS
            "iPhone3,1": "iPhone 4",                  // iPhone 4 GSM
            "iPhone3,2": "iPhone 4",                  // iPhone 4 GSM 2012
            "iPhone3,3": "iPhone 4",                  // iPhone 4 CDMA For Verizon,Sprint
            "iPhone4,1": "iPhone 4S",                 // iPhone 4S
            "iPhone5,1": "iPhone 5",                  // iPhone 5 GSM
            "iPhone5,2": "iPhone 5",                  // iPhone 5 Global
            "iPhone5,3": "iPhone 5c",                 // iPhone 5c GSM
            "iPhone5,4": "iPhone 5c",                 // iPhone 5c Global
            "iPhone6,1": "iPhone 5s",                 // iPhone 5s GSM
            "iPhone6,2": "iPhone 5s",                 // iPhone 5s Global
            "iPhone7,1": "iPhone 6 Plus",             // iPhone 6 Plus
            "iPhone7,2": "iPhone 6",                  // iPhone 6
            "iPhone8,1": "iPhone 6S",                 // iPhone 6S
            "iPhone8,2": "iPhone 6S Plus",            // iPhone 6S Plus
            "iPhone8,4": "iPhone SE", // iPhone SE
            "iPhone9,1": "iPhone 7",                  // iPhone 7 A1660,A1779,A1780
            "iPhone9,3": "iPhone 7",                  // iPhone 7 A1778
            "iPhone9,2": "iPhone 7 Plus",             // iPhone 7 Plus A1661,A1785,A1786
            "iPhone9,4": "iPhone 7 Plus",             // iPhone 7 Plus A1784
            "iPhone10,1": "iPhone 8",                  // iPhone 8 A1863,A1906,A1907
            "iPhone10,4": "iPhone 8",                  // iPhone 8 A1905
            "iPhone10,2": "iPhone 8 Plus",             // iPhone 8 Plus A1864,A1898,A1899
            "iPhone10,5": "iPhone 8 Plus",             // iPhone 8 Plus A1897
            "iPhone10,3": "iPhone X",                  // iPhone X A1865,A1902
            "iPhone10,6": "iPhone X",                  // iPhone X A1901

            /* iPad */
            "iPad1,1": "iPad 1 ",                   // iPad 1
            "iPad2,1": "iPad 2 WiFi",               // iPad 2
            "iPad2,2": "iPad 2 Cell",               // iPad 2 GSM
            "iPad2,3": "iPad 2 Cell",               // iPad 2 CDMA (Cellular)
            "iPad2,4": "iPad 2 WiFi",               // iPad 2 Mid2012
            "iPad2,5": "iPad Mini WiFi",            // iPad Air WiFi
            "iPad2,6": "iPad Mini Cell",            // iPad Mini GSM (Cellular)
            "iPad2,7": "iPad Mini Cell",            // iPad Mini Global (Cellular)
            "iPad3,1": "iPad 3 WiFi",               // iPad 3 WiFi
            "iPad3,2": "iPad 3 Cell",               // iPad 3 CDMA (Cellular)
            "iPad3,3": "iPad 3 Cell",               // iPad 3 GSM (Cellular)
            "iPad3,4": "iPad 4 WiFi",               // iPad 4 WiFi
            "iPad3,5": "iPad 4 Cell",               // iPad 4 GSM (Cellular)
            "iPad3,6": "iPad 4 Cell",               // iPad 4 Global (Cellular)
            "iPad4,1": "iPad Air WiFi",             // iPad Air WiFi
            "iPad4,2": "iPad Air Cell",             // iPad Air Cellular
            "iPad4,4": "iPad Mini 2 WiFi",          // iPad mini 2 WiFi
            "iPad4,5": "iPad Mini 2 Cell",          // iPad mini 2 Cellular
            "iPad4,6": "iPad Mini 2 China",         // iPad mini 2 ChinaModel
            "iPad4,7": "iPad Mini 3 WiFi",          // iPad mini 3 WiFi
            "iPad4,8": "iPad Mini 3 Cell",          // iPad mini 3 Cellular
            "iPad4,9": "iPad Mini 3 China",         // iPad mini 3 ChinaModel
            "iPad5,1": "iPad Mini 4 WiFi",          // iPad Mini 4 WiFi
            "iPad5,2": "iPad Mini 4 Cell",          // iPad Mini 4 Cellular
            "iPad5,3": "iPad Air 2 WiFi",           // iPad Air 2 WiFi
            "iPad5,4": "iPad Air 2 Cell",           // iPad Air 2 Cellular
            "iPad6,3": "iPad Pro 9.7inch WiFi",     // iPad Pro 9.7inch WiFi
            "iPad6,4": "iPad Pro 9.7inch Cell",     // iPad Pro 9.7inch Cellular
            "iPad6,7": "iPad Pro 12.9inch WiFi",    // iPad Pro 12.9inch WiFi
            "iPad6,8": "iPad Pro 12.9inch Cell"]    // iPad Pro 12.9inch Cellular

        if let deviceName = deviceCodeDic[code] {
            return deviceName
        } else {
            if code.range(of: "iPod") != nil {
                return iPodTouch
            } else if code.range(of: iPad) != nil {
                return iPad
            } else if code.range(of: iPhone) != nil {
                return iPhone
            } else {
                return "unknownDevice"
            }
        }
    }

    static let baseScreenWidth: CGFloat = 320.0

    static var bounds: CGRect {
        return UIScreen.main.bounds
    }

    static var screenWidth: CGFloat {
        return UIScreen.main.bounds.size.width
    }

    static var screenHeight: CGFloat {
        return UIScreen.main.bounds.size.height
    }

    static var statusBarHeight: CGFloat {
        return UIApplication.shared.statusBarFrame.height
    }

    static var screenRatio: CGFloat {
        return UIScreen.main.bounds.size.width / baseScreenWidth
    }

    static func navBarHeight(navigationController: UINavigationController) -> CGFloat {
        return navigationController.navigationBar.frame.size.height
    }

    static func commonTopHeight(navigationController: UINavigationController) -> CGFloat {
        return navBarHeight(navigationController: navigationController) + statusBarHeight
    }
}


【2018年版】iOSアプリ新規で作る時にやること②

これ年に10回くらいはやるんだけど、どうにかならんのかなぁ。シェルスクリプト書けばいけそうだけど、Xcodeについてくのだるいし、労力と比較して迷う。

Carthage / RxSwift / Compass / XCGLogger / Reachability / Alamofire / R.swift / SwiftLint / Generamba / VIPER / fastlaneあたりが技術キーワードでしょうか。箇条書きでいきまっせ。
ぼくはもうCarthage対応してないやつは使わないので、Cocoapodsは出てきません。

②Generamba編です。

①はこちら
devdevdev.hatenablog.com




Gem

Gemfileを用意

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "generamba"
# Xcode9.3以降はこう
gem 'generamba', github: "surfstudio/Generamba", branch: "develop"

gem "fastlane"
bundle install --path vendor/bundler

Generamba

セットアップ

bundle exec generamba setup

で、質問に答えましょう。

秘蔵のtemplateをプロジェクトに投入。以下をベースにカスタムしてます。
https://github.com/rambler-digital-solutions/Generamba

作るときは以下

bundle exec generamba gen Hoge template_name

【2018年版】iOSアプリ新規で作る時にやること①

これ年に10回くらいはやるんだけど、どうにかならんのかなぁ。シェルスクリプト書けばいけそうだけど、Xcodeについてくのだるいし、労力と比較して迷う。

Carthage / RxSwift / Compass / XCGLogger / Reachability / Alamofire / R.swift / SwiftLint / Generamba / VIPER / fastlaneあたりが技術キーワードでしょうか。箇条書きでいきまっせ。
ぼくはもうCarthage対応してないやつは使わないので、Cocoapodsは出てきません。

①プロジェクト作成~設定・各種ライブラリ導入編です。



Apple Developerアカウントもらう

誰かにもらいましょう

App ID決める

誰かに決めてもらいましょう

PROJECT作る

作りましょう。

.gitignore投入

### https://raw.github.com/github/gitignore/160d27e2bebf784c4f4a1e070df057f3868b62bc/Objective-C.gitignore

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/

## Other
*.moved-aside
*.xcuserstate

## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
Carthage/Checkouts
Carthage/Build

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

*.generated.swift

vendor/bundler



PROJECT→Info

iOS Deployment Target

誰かに決めてもらいましょう

Configurations

Debug→ProductionDebug
Release→ProductionRelease
に変更して
それぞれをDuplicateして
StagingDebugとStagingReleaseを作る。
Developが必要ならDevelopDebugとDevelopReleaseも。
でもDevelopはあんま作りたくない。なんでiOSエンジニアがローカルにバックエンドの開発環境作らなきゃいけないのかうんぬんかんぬん。
Productionも正直用意するか迷う。何人かで開発するときはいらないかも。

Schema

デフォルトでできてるやつにsuffix:_Productionをつける
Duplicateして_Stagingを作る
両方Sharedにする
_Stagingのほうを編集。Build ConfigurationをStagingDebugとStagingReleaseに変更。

PROJECT→Build Settings

Versioning

Current Project Version:1
Versioning System:Apple Generic
に変更。

Swift Compiler - Custom Flags→Other Swift Flags

ProductionDebug:-D DEBUG
StagingDebug: -D DEBUG -D STAGING
追加。

TARGETS→General

Identity→Version
Signing
Deployment Info
をいじる
Deployment Info→Main Interfaceはクリアして、Main.storyboardを削除。

TARGETS→Capabilities

必要なのをオン

TARGETS→Info

Bundle Name:$(BundleNamePrefix)アプリ名

TARGETS→Build Settings

User-DefinedにBundleNamePrefixとConfigurationNameを追加。Stagingだけ設定をお好みで追加。
Packaging→Product Bundle Identifier:[お好みで]$(ConfigurationName)

ディレクトリ整理

めんどいので省略。



SwiftLint導入

インストール。グローバルにいれるのすきじゃないんだけど、Build Phaseで使いたいのでまぁしゃーない。もう、homebrewのインストールはいいっしょ。。このブログ内検索すればあるはず。

brew install swiftlint

TARGETS→Build PhaseでNew Run Script Phase

#!/bin/sh

if which swiftlint >/dev/null; then
  swiftlint autocorrect
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

で、プロジェクトルートに.swiftlint.ymlを用意。

included:
  - [Project Module Nameを]

excluded:
  - Pods/
  - Carthage/
  - fastlane
  - [Project Module Nameを]/Application/R.generated.swift

disabled_rules:
  - force_cast
  - force_try
  - force_unwrapping

file_length:
  warning: 1000
  error: 1500
type_body_length:
  warning: 500
  error: 750
function_body_length:
  warning: 200
  error: 300
line_length:
  warning: 200
  error: 300
variable_name:
  min_length: 1

で、ビルドエラーになったら修正。



Carthage導入

インストール。

brew install carthage

プロジェクトルートにCartfileを作る。基本は以下。
バージョンは2018/06現在の最新。

# General
github "ReactiveX/RxSwift" "4.2.0"
github "hyperoslo/Compass" "6.0.0"

# Resources
github "mac-cain13/R.swift.Library" "v4.0.0"

# Debug
github "DaveWoodCom/XCGLogger" "6.0.4"

# API
github "ashleymills/Reachability.swift" "v4.1.0"
github "Alamofire/Alamofire" "4.7.2"

作れたら

carthage update --cache-builds --platform iOS

Cartfileを変更したら毎回これやりましょう。初回はものすごい時間がかかりまっせ。
TARGETS→General→Linked Frameworks and Librariesで+→Add Other...で
Carthage/Build/iOSの下にできた.frameworkたちを追加。

f:id:devdevdev:20180628075150p:plain

TARGETS→Build PhaseでNew Run Script Phase

/usr/local/bin/carthage copy-frameworks

Input Filesにさっき入れたFrameworkを全て追加。。。

f:id:devdevdev:20180628075608p:plain

・7/1追記
github.com
公式の説明の通り、ドラッグ&ドロップだとなんかうまくいかなかった。
たぶんOutput Filesもちゃんと書いたほうがよい気がする



R.swift導入

github.com
ここから任意のバージョンのzipをダウンロードして解凍。
rswiftをプロジェクト内のお好みの場所に。

TARGETS→Build PhaseでNew Run Script Phase
これはComple Sourcesより前にやったほうがよい(上から順番に実行されるから上のほうにおくとよい)と思う。

"$PROJECT_DIR/rswiftのパス" generate "$PROJECT_DIR/R.generated.swiftを置きたいディレクトリのパス(.swiftlint.yml参照)"

ビルドするとR.generated.swiftができるので、プロジェクトに追加しておきましょう。



【Android】SimpleRecyclerViewAdapter

めずらしくあんどろいだーーーーーー
ねいてぃぶはらくちんでよいね!!!最高!ねむい!体調悪い!つらい!でも納期も時間も待ってくれない!

はい、RecyclerViewAdapterね。簡単なやつならこうしましょう。



app/build.gradle

抜粋です。databindingも使うのでね。

apply plugin: 'kotlin-kapt'
android {
    dataBinding {
        enabled = true
    }
}
dependencies {
    ext.android_support_version = "27.1.1"
    implementation "com.android.support:recyclerview-v7:$android_support_version"
}



SimpleRecyclerViewAdapter.kt

おもむろに以下をじっそうしましょう。

package hoge;

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import java.lang.reflect.ParameterizedType

abstract class SimpleRecyclerViewAdapter<V : View, I : Any?>(defaultItems: ArrayList<I>? = null) : RecyclerView.Adapter<SimpleRecyclerViewAdapter<V, I>.ItemViewHolder>() {

    protected val isEmpty get() = !items.any()

    private val items: ArrayList<I> = arrayListOf()

    init {
        defaultItems?.let { items.addAll(it) }
    }

    fun removeAll() {
        items.clear()
        notifyDataSetChanged()
    }

    fun addItem(item: I) {
        items.add(item)
        notifyItemInserted(items.count())
    }

    fun removeItem(item: I) {
        val index = items.indexOf(item)
        items.removeAt(index)
        notifyItemRemoved(index)
    }

    fun updateItem(position: Int, item: I) {
        items[position] = item
        notifyItemChanged(position)
    }

    final override fun getItemCount() = items.count()

    final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(onCreateItemView(parent.context).apply {
        layoutParams = RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.MATCH_PARENT,
                RecyclerView.LayoutParams.WRAP_CONTENT
        )
    })

    @Suppress("UNCHECKED_CAST")
    final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        (holder.itemView as? V)?.let {
            onBindItemView(it, items[position], position)
        }
    }

    @Suppress("UNCHECKED_CAST")
    protected open fun onCreateItemView(context: Context) =
            ((javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<*>)
                    .constructors.first().newInstance(context) as V

    protected abstract fun onBindItemView(itemView: V, item: I, position: Int)

    inner class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view)
}



使い方

view_hoge_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textViewTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
HogeListItemView.kt
package hoge

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import hoge.R
import kotlinx.android.synthetic.main.view_hoge_list_item.view.*

internal class HogeListItemView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : LinearLayout(context, attrs, defStyleAttr) {

    init {
        LayoutInflater.from(context).inflate(R.layout.view_hoge_list_item, this)
    }

    fun setTitle(title: String) {
        textViewTitel.text = title
    }
}



HogeFragment.kt

省略。Bindingのサンプル書くのめんどい。

HogeFragmentVM.kt
package hoge

import android.content.Context
import hoge.SimpleRecyclerViewAdapter
import hoge.HogeListItemView

internal class HogeFragmentVM() {
    val recyclerViewAdapter = object : SimpleRecyclerViewAdapter<HogeListItemView, String>(arrayListOf<String>("Hogeeeeee", "Aaaaaaaaaaa")) {
        override fun onBindItemView(itemView: HogeListItemView, item: String, position: Int) {
            itemView.setTitle(item)
        }
    }
}
fragment_hoge.xml

抜粋。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="vm"
            type="hoge.HogeFragmentVM" />
    </data>
    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:recyclerViewAdapter="@{vm.recyclerViewAdapter}" />
</layout>


onBindItemViewだけoverrideすればいいから簡単ですね。はい。いじょー