Storyboardを使ってUITableViewを組み立てる場合のテンプレート(Swift3)

環境: Swift3

よく使うのでコピペ用にメモしておく。

ViewController

import UIKit

class ViewController: UIViewController {

    var items: [String] = ["foo", "bar", "hoge"]
    
    @IBOutlet weak var tableView: UITableView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
        cell.item = self.items[indexPath.row]
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
}

final class MyCell: UITableViewCell {
    
    var item: String? {
        didSet {
            self.nameLabel?.text = self.item
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
    }

    @IBOutlet weak var nameLabel: UILabel?
}

カスタムセルMyCellを定義する。
Storyboard 上でカスタムセルを貼り付けて定義している場合は以下のような register メソッドは不要。
逆にregister メソッドで登録してしまうとカスタムセルが表示されなくなってしまうので注意。
カスタムセルをコードのみで定義、または別途 nib ファイルで定義した場合は viewDidLoad などで以下のように register メソッドで登録する。

self.tableView?.register(MyCell.self, forCellReuseIdentifier: "MyCell")
self.tableView?.register(UINib(nibName: "MyCell", bundle: nil), forCellReuseIdentifier: "MyCell")

また、普通やらないと思うが、カスタムセルを使う場合に UITableViewCell にデフォルトで用意されているプロパティ(textLabelなど)を使うと表示がおかしくなるのでやらないこと。

Storyboard

UIViewController に UITableView と UITableViewCell を貼り付ける。
UITableView の datasource と delegate を UIViewController に接続する。
UITableViewCell のクラス名を設定、Identifier を設定。
UITableViewCell 上にラベルなどがあればそれも接続する。

f:id:xyk:20170115202101p:plain f:id:xyk:20170115202043p:plain

Extension

Cell・HeaderFooterViewのregisterやdequeueのIdentifierは文字列で扱うが、大抵はクラス名をそのまま使用するので、文字列ではなく、クラスを使って扱えるようにExtensionを定義する。

定義

extension UITableView {

    // func dequeueReusableCell(withIdentifier identifier: String, for indexPath: IndexPath) -> UITableViewCell
    // の代わりに使用する
    func dequeueReusableCell<T: UITableViewCell>(withClass type: T.Type, for indexPath: IndexPath) -> T {
        return self.dequeueReusableCell(withIdentifier: String(describing: type), for: indexPath) as! T
    }

    // func dequeueReusableHeaderFooterView(withIdentifier identifier: String) -> UITableViewHeaderFooterView?
    // の代わりに使用する
    func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(withClass type: T.Type) -> T {
        return self.dequeueReusableHeaderFooterView(withIdentifier: String(describing: type)) as! T
    }

    // func register(_ nib: UINib?, forCellReuseIdentifier identifier: String)
    // func register(_ cellClass: Swift.AnyClass?, forCellReuseIdentifier identifier: String)
    // の代わりに使用する
    func register(tableViewCellClass cellClass: AnyClass) {
        let className = String(describing: cellClass)
        if UINib.fileExists(nibName: className) {
            self.register(UINib.cachedNib(nibName: className), forCellReuseIdentifier: className)
        } else {
            self.register(cellClass, forCellReuseIdentifier: className)
        }
    }

    // func register(_ nib: UINib?, forHeaderFooterViewReuseIdentifier identifier: String)
    // func register(_ aClass: Swift.AnyClass?, forHeaderFooterViewReuseIdentifier identifier: String)
    // の代わりに使用する
    func register(headerFooterViewClass aClass: AnyClass) {
        let className = String(describing: aClass)
        if UINib.fileExists(nibName: className) {
            self.register(UINib.cachedNib(nibName: className), forHeaderFooterViewReuseIdentifier: className)
        } else {
            self.register(aClass, forHeaderFooterViewReuseIdentifier: className)
        }
    }
}


extension UINib {

    static let nibCache = NSCache<NSString, UINib>()

    static func fileExists(nibName: String) -> Bool {
        return Bundle.main.path(forResource: nibName, ofType: "nib") != nil
    }

    static func cachedNib(nibName: String) -> UINib {
        if let nib = self.nibCache.object(forKey: nibName as NSString) {
            return nib
        } else {
            let nib = UINib(nibName: nibName, bundle: nil)
            self.nibCache.setObject(nib, forKey: nibName as NSString)
            return nib
        }
    }
}

使用時

// Cellの登録
// tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")
tableView.register(tableViewCellClass: MyCell.self)

// Cellの取得
// let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
let cell = tableView.dequeueReusableCell(withClass: MyCell.self, for: indexPath)

// HeaderFooterViewの登録(MyHeaderView.nibを使用)
// let className = String(describing: MyHeaderView.self)
// tableView.register(UINib(nibName: className, bundle: nil), forCellReuseIdentifier: className)
tableView.register(headerFooterViewClass: MyHeaderView.self)

// HeaderFooterViewの取得
// let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "MyHeaderView") as! MyHeaderView
let view = tableView.dequeueReusableHeaderFooterView(withClass: MyHeaderView.self)

Fastlaneでplistを更新する

fastlane: 2.3.0

Fastlaneでplistを更新する方法を調べたのでメモ。
update_info_plistというアクションを使う。
fastlane/update_info_plist.rb at master · fastlane/fastlane · GitHub

app_identifierdisplay_nameについてはアクションのパラメータに直接渡せるが、それ以外は更新するロジックをブロックに書いて渡す。

以下はURL Schemeを環境別に更新する例。
正確なパラメータ名はplistファイルをxmlエディタで開いて確認する。

Fastfile

platform :ios do

  lane :update_url_scheme_dev do
    update_url_scheme(scheme: 'Dev')
  end

  lane :update_url_scheme_beta do
    update_url_scheme(scheme: 'Beta')
  end

  private_lane :update_url_scheme do |options|
    update_info_plist(
            plist_path: 'path/to/Info.plist',
            block: lambda { |plist|
              plist['CFBundleURLTypes'].each {|urlType|
                if urlType['CFBundleURLName'] == 'com.example.default-url-identifier'
                  urlSchemes = urlType['CFBundleURLSchemes']
                  urlSchemes.map! {|urlScheme|
                    TARGET_URL_SCHEME = 'myapp'
                    if urlScheme.start_with?(TARGET_URL_SCHEME)
                      "#{TARGET_URL_SCHEME}-#{options[:scheme].downcase}"
                    else
                      urlScheme
                    end
                  }
                end
              }
            }
    )
  end

end

実行

$ fastlane ios update_url_scheme_beta

XcodeをAppStoreを使わずインストールしたときのメモ

Appleアカウントでログインし、以下からダウンロードする。

https://developer.apple.com/download/
https://developer.apple.com/download/more/
xip ファイルを選択する。
リリース直後だとかなり時間がかかる。
Chrome でダウンロードし、展開しようとしたところ
アーカイブXcode_8.2.xip”は壊れているため展開できません。」
と出て失敗した。何回かやっても同様な現象が出てハマった。
一見、ダウンロードが正常に終了したように見えるのだが、どうやら途中で止まり異常終了していたようだ。
Safari でダウンロードしてみたところ、やはり途中で止まってしまったが右上にあるアイコンから
ダウンロード状況を確認でき、再開させて無事ダウンロードすることができた。
f:id:xyk:20161214153025p:plain で、ダウンロードした xip ファイルをそのまま展開しようとすると
GateKeeperによるファイルの検証が始まりこれがまた時間がかかる。
これは xattr コマンドでファイルの拡張属性com.apple.quarantineを削除することでスキップすることができる。

削除前

$ xattr Xcode_8.2.xip
com.apple.metadata:kMDItemDownloadedDate
com.apple.metadata:kMDItemWhereFroms
com.apple.quarantine

com.apple.quarantine属性を削除

$ xattr -d com.apple.quarantine Xcode_8.2.xip

削除後

$ xattr Xcode_8.2.xip
com.apple.metadata:kMDItemDownloadedDate
com.apple.metadata:kMDItemWhereFroms

UIViewに角丸な枠線(破線/点線)を設定する

環境: Swift3

UIViewの角を丸くした枠線を書くには以下のように書けばよい。

let roundView = UIView()
roundView.backgroundColor = .lightGray
roundView.layer.borderColor = UIColor.blue.cgColor
roundView.layer.borderWidth = 3
roundView.layer.cornerRadius = 10
roundView.layer.masksToBounds = true
// roundView.clipsToBounds = true // 上と同じ

さらに枠線を破線にしたいのでCAShapeLayerを使って以下のように実装した。

以下がプレイグラウンドで確認した完成画像となる。

f:id:xyk:20161128185455p:plain

import UIKit
import PlaygroundSupport

final class DashedBorderAroundView: UIView {
    
    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)
        
        if self.dashedBorderLayer.superlayer == nil {
            self.layer.addSublayer(self.dashedBorderLayer)
            self.layer.cornerRadius = 10
        }
    }
    
    private lazy var dashedBorderLayer: CAShapeLayer = { [unowned self] in
        
        let rect = self.bounds
        let cornerRadius: CGFloat = 10
        
        let path = UIBezierPath()
        
        path.move(to: CGPoint(x: 0, y: rect.maxY - cornerRadius)) // 1
        path.addLine(to: CGPoint(x: 0, y: cornerRadius)) // 2
        path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius), // 3
                    radius: cornerRadius,
                    startAngle: CGFloat(M_PI),
                    endAngle: -CGFloat(M_PI_2),
                    clockwise: true)
        path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: 0)) // 4
        path.addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: cornerRadius), // 5
                    radius: cornerRadius,
                    startAngle: -CGFloat(M_PI_2),
                    endAngle: 0,
                    clockwise: true)
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius)) // 6
        path.addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - cornerRadius), // 7
                    radius: cornerRadius,
                    startAngle: 0,
                    endAngle: CGFloat(M_PI_2),
                    clockwise: true)
        path.addLine(to: CGPoint(x: cornerRadius, y: rect.maxY)) // 8
        path.addArc(withCenter: CGPoint(x: cornerRadius, y: rect.maxY - cornerRadius), // 9
                    radius: cornerRadius,
                    startAngle: CGFloat(M_PI_2),
                    endAngle: CGFloat(M_PI),
                    clockwise: true)
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.blue.cgColor
        shapeLayer.lineWidth = 3
        shapeLayer.lineDashPattern = [3, 6]
        shapeLayer.lineCap = kCALineJoinRound
        shapeLayer.path = path.cgPath
        
        return shapeLayer
    }()
    
}

let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .white

let dView = DashedBorderAroundView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
dView.backgroundColor = .lightGray
dView.center = view.center
view.addSubview(dView)

PlaygroundPage.current.liveView = view

パスの描画部分がわかりづらいので図で補足説明を追加。

回転方向
f:id:xyk:20170308104206j:plain:w300

パスの順番
f:id:xyk:20170308103340j:plain:w300

アプリサブミット時の輸出コンプライアンスの確認をスキップする

アプリをiTunes Connectでサブミットするときに以下のように毎回「輸出コンプライアンス」、「広告ID」についての質問に回答する必要がある。
少しの手間だが面倒なのでこの入力をスキップする方法を調べた。

f:id:xyk:20161128175259p:plain

暗号化機能を含まない場合はInfo.plistITSAppUsesNonExemptEncryptionというキーでNOを設定しておくことで「輸出コンプライアンス」の項目が表示されなくなる。

UIWebViewのリクエストにUserAgentを設定する

環境: Swift3

UIWebViewのリクエストにUserAgentを設定するには、リクエスト前にUserDefaultsのregisterメソッドでキー名UserAgentで値をセットする必要がある。

UserDefaults.standard.register(defaults: ["UserAgent" : "hoge"])

この時、

// これだと設定されない
UserDefaults.standard.set("hoge", forKey: "UserAgent")

のようにUserDefaultsに普通に保存してもダメでregisterメソッドでデフォルト値として設定すること。

// これを使う
open func register(defaults registrationDictionary: [String : Any])

setメソッドとregisterメソッドの違いは以下参照。
xyk.hatenablog.com


追記

ハマリポイントがあったのでメモ。

UIWebViewで使われるUserAgentを取得する方法は以下。

let ua: String? = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")

取得結果は

Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0 Mobile/14C92 Safari/602.1

となった。
この結果の末尾に独自の文字列(ここでは例としてhoge)を追加して

Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0 Mobile/14C92 Safari/602.1 hoge

これを新たなUserAgentとして更新しようとしたがなぜか更新できなかった。
どうやら使用するUIWebViewのインスタンスに対してwebView.stringByEvaluatingJavaScript(from: "navigator.userAgent")を呼び出すとそれ以降、UserAgentを設定しても更新できないようだ。

なので、以下のようにした。

class WebViewController: UIViewController {

   @IBOutlet weak var webView: UIWebView?

    override func loadView() {
        
        self.updateUserAgent()
        super.loadView()
    }

    func updateUserAgent() {
        guard
            let currentUserAgent = UIWebView().stringByEvaluatingJavaScript(from: "navigator.userAgent"),
            !currentUserAgent.contains("hoge")
        else {
            return
        }
        
        let newUserAgent = "\(currentUserAgent) hoge"
        let dic = ["UserAgent" : newUserAgent]
        UserDefaults.standard.register(defaults: dic)
    }

}

実際に使用するself.webViewには触れず、またself.webViewを使用する前に、新たなUIWebViewインスタンスを作って、そこから元のUserAgentを取り出して更新するようにした。

一度更新すればアプリ内では引き継がれるのでapplication:didFinishLaunchingWithOptionsでやってしまっても良いと思う。

UserDefaultsのregisterDefaultsメソッドについて

環境: Swift3

UserDefaultsのregisterDefaultsメソッドについて勘違いしていたのでメモ。

// Swift3 で registerDefaults() から register(defaults: ) に変更になった
open func register(defaults registrationDictionary: [String : Any])

このregisterメソッドを使って登録したDictionary(以降RegistrationDictionaryと呼ぶ)はデフォルト値として使う用でUserDefaultsのデータとしてファイルに書き込まれるわけではない。
あるキーで読み込みした時にそのキーがまだUserDefaultsに存在せず、RegistrationDictionaryに存在すれば、RegistrationDictionaryの値をデフォルト値として返す。
既にキーがUserDefaultsに登録されていた場合、またはその後、そのキーでUserDefaultsに登録された場合は、UserDefaultsの値を返す。

let defaults = UserDefaults.standard

let foo1 = defaults.string(forKey: "Foo")
print("foo1:", foo1) // nil

defaults.register(defaults: ["Foo": "Bar"])

let foo2 = defaults.string(forKey: "Foo")
print("foo2:", foo2) // Optional("Bar")

UserDefaults.standard.set("Hoge", forKey: "Foo")
defaults.synchronize()

let foo3 = defaults.string(forKey: "Foo")
print("foo3:", foo3) // Optional("Hoge")