2016年4月28日木曜日

[RxSwift] SectionありのDataSourceを生成する

本家のサンプルにも記載されている通り、RxDataSourcesを使用する方法があります。
https://github.com/ReactiveX/RxSwift/tree/master/RxExample/RxDataSources

RxDataSources:
https://github.com/RxSwiftCommunity/RxDataSources

Podfileに記載するなりしてインストールしてください。

SectionありのdataSourceを生成する

RxTableViewSectionedReloadDataSourceを使用します。SectionModelでSectionに表示するelementを指定します。

let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, String>>()
let items = Observable.just([
    SectionModel(model: "First section", items: [
            "aaaa",
            "bbbb",
        ]),
    SectionModel(model: "Second section", items: [
            "cccc",
            "dddd",
        ])
    ])


セルの生成

以下のように、indexPathとelementが引数に入っているのでコールバック内でセルを生成する。
dataSource.configureCell = { (_, tableView, indexPath, element) in
    let cell = tv.dequeueReusableCellWithIdentifier("Cell")!
    cell.textLabel?.text = "\(element) @ row \(indexPath.row)"
    return cell
}


データとTablewViewをBindする

items
    .bindTo(tableView.rx_itemsWithDataSource(dataSource))
    .addDisposableTo(disposeBag)


ヘッダーのカスタマイズ

UITableViewDelegateで処理する必要があります。
以下のように、ヘッダー用のViewとHightを返すモジュールを実装します。
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let view = UITableViewHeaderFooterView()
    view.textLabel?.text = dataSource.sectionAtIndex(section).model ?? ""
    return view
}
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 44
}

2016年4月16日土曜日

[RxSwift] UITextFieldの入力テキストと通知

ViewModel側にObserverを作成する。ObserverでText更新を受け取ってViewModelの処理を行う。


let disposeBag = DisposeBag()
var account:Variable = Variable("")

// 参照方法
account.value



UITextFieldのrx_textとViewModelのObserverをBindする。
Textが更新されると、ViewModel側のObserverまで届く。
let disposeBag = DisposeBag()
accountTextField.rx_text
  .bindTo(viewModel.accountSubject.asObserver())
  .addDisposableTo(disposeBag)


プログラム上でTextを更新する場合(ストリームに流す場合)、sendActionsForControlEventsでValueChangedイベントをUITextFieldに流し込む。
accountTextField.text = "test"
accountTextField.sendActionsForControlEvents(.ValueChanged)

2016年4月15日金曜日

[RxSwift] UITableViewでカスタムセルを使用する

UITableViewで表示するデータをVariableで宣言するし、データの更新はvalueにセットする。

    let items = Variable<[Item]>([])
    // 新しいデータで更新
    self.items.value = updateItems

UITableViewにデータをBindingするには、rx_itemsWithCellIdentifierをコールする。
カスタムセルを使用する場合は、第二引数のcellTypeにセットが必要です。

    viewModel?.items
        .asDriver()
        .drive(tableView.rx_itemsWithCellIdentifier("CustomTableViewCell",cellType: CustomTableViewCell.self)) { row, item, cell in
            cell.item = item
            cell.tag = row
        }
        .addDisposableTo(disposeBag)


セルの選択はrx_itemSelectedを使用します。
tableView.rx_itemSelected
    .subscribeNext { [unowned self](indexPath) in
        self.tableView.deselectRowAtIndexPath(indexPath, animated: true)
        // get cell
        let cell: CustomTableViewCell = self.tableView.cellForRowAtIndexPath(indexPath)! as! CustomTableViewCell        
    }
    .addDisposableTo(disposeBag)

EditingStyleのセット

rx_itemsWithCellIdentifierを使用すると、DefaultでEditting可能になってしまいます。
不要の場合はUITableViewCellEditingStyle.Noneをセットしましょう。

func tableView(tableView: UITableView, editingStyleForRowAtIndexPath: NSIndexPath) -> UITableViewCellEditingStyle {
    return UITableViewCellEditingStyle.None
}

2016年4月12日火曜日

[Swift]循環参照(相互参照)によるメモリリーク

MVVM移行時にクラス分けを行った結果、ViewControllerのメモリリークが発生しました。deinitが呼ばれず期待動作となりませんでした。


循環参照(相互参照)によるメモリリーク

以下のような状態になると、メモリリークが発生します。
何も考えずにViewModelのDelegateを追加、DelegateにViewControllerをセットでメモリリーク発生


解決策

弱参照になるようにDelegate用プロトコルをclass継承し、weakで保持できるように対応。


protocolにclass継承追加
 public protocol ViewModelDelegate : class{
 }


weak追加
 weak var delegate: ViewModelDelegate?

2016年4月3日日曜日

[Swift]UIButtonでPressed状態の装飾を行う

AndroidならSelectorのxmlファイルを作成すればボタンの状態に合わせた装飾できます。
Swiftではボタンの文字などは次のfuncで設定可能です。

func setTitle(_ title: String?,forState state: UIControlState)
func setTitleColor(_ color: UIColor?,forState state: UIControlState)
func setTitleShadowColor(_ color: UIColor?,forState state: UIControlState)
func setBackgroundImage(_ image: UIImage?,forState state: UIControlState)

layerなどで装飾をする場合は、次のようにhighlightedのdidSetで変更可能です。

@IBDesignable class MyUIButton: UIButton {
    
    @IBInspectable var borderColor :  UIColor = UIColor.blackColor()
    @IBInspectable var borderHighLightedColor :  UIColor = UIColor.clearColor()
    
    override internal func awakeFromNib() {
        super.awakeFromNib()
    }
    
    override var highlighted: Bool{
        didSet{
            if (highlighted) {
                self.layer.borderColor = borderHighLightedColor.CGColor
            } else {
                self.layer.borderColor = borderColor.CGColor
            }
        }
    }
    // Attributes Inspectorで設定した値を反映
    override func drawRect(rect: CGRect) {
        self.layer.borderColor = borderColor.CGColor
    }

}

2016年3月27日日曜日

[Swift] Keyboardの表示に合わせてScrollViewの高さを変更する

Androidの場合、Keyboardが表示されると自動的にアプリケーションWIndowがリサイズされてKeyboardに被らないように表示されます。
iOSでは自前で実装する必要があります。

以下は、ScrollVIewを利用したサンプルです。
Keyboardが表示されたタイミングで、ScrollViewのcontentInsetとscrollIndicatorInsetsを変更します。

class AdjustScrollViewControll : NSObject{
    var scrollView : UIScrollView?
    
    init(scrollView : UIScrollView){
        self.scrollView = scrollView
    }
    
    func addObservers(){
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector: #selector(self.willShowNotification(_:)), name: UIKeyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(self.willHideNotification(_:)), name: UIKeyboardWillHideNotification, object: nil)
    }
    
    func removeObserviers(){
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
    
    func willShowNotification(notification: NSNotification) {
        
        let info = notification.userInfo
        let infoNSValue = info![UIKeyboardFrameEndUserInfoKey] as! NSValue
        let kbSize = infoNSValue.CGRectValue().size
        let contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height + 8, 0.0)
        scrollView!.contentInset = contentInsets
        scrollView!.scrollIndicatorInsets = contentInsets
    }
    
    func willHideNotification(notification: NSNotification) {
        let contentInsets = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0)
        scrollView!.contentInset = contentInsets
        scrollView!.scrollIndicatorInsets = contentInsets
    }
}


ViewControllerからは次のようにコールします。
class HiddingKeyboardViewController: UIViewController,UITextFieldDelegate {

    @IBOutlet weak var scrollView: UIScrollView!
    
    var adjustTextFieldControll : AdjustScrollViewControll?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        adjustTextFieldControll = AdjustScrollViewControll(scrollView: scrollView)
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        
        adjustTextFieldControll!.addObservers()
    }
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        adjustTextFieldControll!.removeObserviers()
    }
    
}

2016年3月26日土曜日

[Swift]アスペクト比を維持しつつUIImageViewのサイズを変更する

TableViewCellの実装で、次のような画面幅に合わせた画像を表示する際に困ったのでメモ。



Androidでは次のように指定することで、高さが自動で確定することができました。

<ImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:adjustViewBounds="true"/>

が、iOSでは簡単にはできず、カスタムViewを作ることで解決しました。
UIImageViewのimageがセットされる際に、アスペクト比を計算してConstraintを確定しているだけです。

class AspectAdjuestUIImageView: UIImageView {
    
    internal var aspectConstraint : NSLayoutConstraint? {
        didSet {
            if oldValue != nil {
                self.removeConstraint(oldValue!)
            }
            if aspectConstraint != nil {
                self.addConstraint(aspectConstraint!)
            }
        }
    }
    
    override var image: UIImage?{
        willSet{
            let aspect = newValue!.size.width / newValue!.size.height
            
            aspectConstraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.Height, multiplier: aspect, constant: 0.0)
        }
    }
}