カテゴリー
iOS Swift

UITableViewのCellのReorder、Swipe Editの結果をCoreDataに保存

Sample Code on github

UITableViewのCellのReorder(順番組み替え)とSwipe editの動作の結果をCoreDataに保存するサンプルコードを書いてみました。

CoreDataにsongs: [Song]を保存するためのマスターオブジェクトのProjectを用意しました。

CoreDataのProject EntityのRelationshipにsongsを設定し、To Manyとし、Orderedにチェックを入れると、songsの型はXCodeが自動でNSOrderedSetに設定します。

extension Project {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Project> {
        return NSFetchRequest<Project>(entityName: "Project")
    }

    @NSManaged public var title: String?
    @NSManaged public var songs: NSOrderedSet?

}

カテゴリー
iOS Swift

UIAlertControllerをPresentする

@objc private func addTapped() {
  var alertTextField: UITextField?
  let alert = UIAlertController(title: "Enter New Song Title", message: nil, preferredStyle: UIAlertController.Style.alert)
  alert.addTextField(configurationHandler: { (textField: UITextField!) in
    alertTextField = textField
  })
  alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel))
  alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default) { _ in
    if let text = alertTextField?.text {
      self.dataService.addNewSong(title: text)
      self.tableView.reloadData()
    }
  })
  self.present(alert, animated: true)
} 
カテゴリー
iOS Swift

UILabelのFontを設定する

// using custom fonts
label.font = UIFont(name: "fontname", size: 50)

// using system fonts
label.font = UIFont.systemFont(ofSize: 20.0) 
label.font = UIFont.boldSystemFont(ofSize: 20.0)
label.font = UIFont.italicSystemFont(ofSize: 20.0)

// modifying only the font size
label.font = label.font.withSize(20)

// adjusting the font size within the label width
label.adjustsFontSizeToFitWidth = true
カテゴリー
iOS Swift

BehaviorRelayとUITableViewのBinding

let dataService = DataService.shared // some singleton class 
let bag = DisposeBag()

@override func viewDidLoad() {


  dataService.someRelay.bind(to: tableView.rx.items(cellIdentifier: CustomTVCell.identifier, cellType: CustomTVCell.self)) { row, object, cell in
    self.dataCount = self.dataService.someRelay.value.count
    cell.configure(index: row, viewWidth: self.viewWidth, viewHeight: self.viewHeight, rowHeight: self.tableView.rowHeight, obj: object)
  }
  .disposed(by: bag)

}
カテゴリー
iOS Swift

UICollectionViewのDrag and Drop

このStanford Universityの授業が参考になりました。

Sample Code

UICollectionViewのCellをDrag & Dropする場合は、専用のAPIが用意されています。UITableViewにもほぼ同じAPIがあります。

まず、collectionView.dragDelegate = selfとすることで、ViewControllerをDelegateに設定することが出来ます。

dragDelegateで重要なのはitemsForBeggingメソッドです。また、dragSessionWillBeginメソッドとdragSessionDidEndメソッドでdragSessionの状態をトラッキング出来るので、それを利用してisDraggingプロパティを更新しています。

extension ViewController: UICollectionViewDragDelegate {
  
  // modifying the isDragging state according to the dragSession lifecycle
  func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) {
    isDragging = true
  }
  
  func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {
    isDragging = false
  }
  
  func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    // in order for session to know the localContext. This will be used in dropSessionDidUpdate method
    session.localContext = collectionView
    let itemProvider = NSItemProvider(object: colors_A[indexPath.item])
    let dragItem = UIDragItem(itemProvider: itemProvider)
    dragItem.localObject = indexPath.item
    return [dragItem]
  }
}

DropDelegateで重要なのは、そのドラッグが開始されたのがそのCollectionViewの中か外かを判別し、中であればDropProposalのoperationを.moveに、外であれば.copyを設定することが出来ます。これにより、同一CollectionView内のドラッグの場合は順番入替(reorder)を、外からのドラッグの場合は新規挿入を、と実装を分けることが可能になります。

また、自身以外のUIViewをドラッグ先(DragDestination)とする場合は以下のように実装します。

class ViewController: UIViewController {

  lazy var trashImageView: UIImageView = {
    let imageView = UIImageView(image: UIImage(systemName: "trash"))
    let dropInteraction = UIDropInteraction(delegate: self)
    imageView.addInteraction(dropInteraction)
    // isUserInteractionEnabled needs to be set to true, otherwise it will not work...
    imageView.isUserInteractionEnabled = true
    imageView.alpha = 0
    imageView.isHidden = !isDragging
    return imageView
  }()

}

extension ViewController: UIDropInteractionDelegate {
  
  func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
    return session.canLoadObjects(ofClass: UIColor.self)
  }
  
  func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
    return UIDropProposal(operation: .copy)
  }
  
  func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
    session.items.forEach({ item in
      if let index = item.localObject as? Int {
      // performBatchUpdates is necessary also to make the changes animate
        collectionView_A.performBatchUpdates {
          colors_A.remove(at: index)
          collectionView_A.deleteItems(at: [IndexPath(item: index, section: 0)])
        }
      }
    })
  }
}
カテゴリー
iOS Swift

UIView.isHiddenをAnimateする

var isDraggingに応じて、とあるview.isHiddenをアニメーションするサンプルコード。

isHiddenだけだとAnimationしないので、alphaをAnimationしています。

var isDragging: Bool = false {
  didSet {
    if isDragging == true {
      UIView.animate(withDuration: 0.5, animations: {
        // make trashImageView.isHidden to false,
        self.trashImageView.isHidden = !self.isDragging
        // then animate the alpha
        self.trashImageView.alpha = 1.0
      }) { _ in
        // do nothing
      }
    } else {
      UIView.animate(withDuration: 0.5, animations: {
        // animate the alpha first,
        self.trashImageView.alpha = 0
      }) { _ in
        // then, making isHidden to true
        self.trashImageView.isHidden = !self.isDragging
      }
    }
  }
}
カテゴリー
iOS Swift

Drag and Dropを学ぶ

参考

【概要】

ドラッグはUIエレメントに対するロングプレスをトリガーとして開始され、プレヴューイメージが表示され、drag session (UIDragSession)が開始される。drag sessionはドラッグ動作が完了もしくはキャンセルされるまで維持される。

プレヴューイメージはDestinationまでドラッグされる。Destinationがドラッグされたアイテムの型を扱える場合はその旨の表示がなされる。

ViewControllerにはdrag delegateやdrop delegateがアサインされる。

ViewController内のドラッグされる個別のviewはinteraction objectsを持つ必要がある。drag source(ドラッグ元)のviewはUIDragInteractionを、drag destination(ドラッグ先)のviewはUIDropInteractionを持つ。これらのInteractionには幅広いレンジのdelegateメソッドがあり、実装に応じてドラッグ・ドロップ・ライフサイクル内の様々な時点で呼ばれる。

各ドラッグ動作はdrag item (UIDragItem)で表される。drag itemはプレヴューイメージやitem provider (UIItemProvider)を持つ。item providerは移動対象のアイテムに関する情報を持ち、それはドロップが実施される場合の非同期トランスファーに使用される。

以下はdrag sourceとdrag destinationの最小コード

import UIKit

class ViewController: UIViewController {

  var stringArray: [String] = [] {
    didSet {
      print(stringArray)
    }
  }
  
  lazy var dragSource: UIView = {
    let view = UIView()
    let dragInteraction = UIDragInteraction(delegate: self)
    view.addInteraction(dragInteraction)
    return view
  }()
  
  lazy var dragDestination: UIView = {
    let view = UIView()
    let dropInteraction = UIDropInteraction(delegate: self)
    view.addInteraction(dropInteraction)
    return view
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    view.addSubview(dragSource)
    view.addSubview(dragDestination)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    layout()
  }
  
  func layout() {
    // dragSource
    let itemSize: CGFloat = 100
    dragSource.frame = CGRect(x: view.bounds.width/2 - itemSize/2, y: view.bounds.height/2 - itemSize/2, width: itemSize, height: itemSize)
    dragSource.backgroundColor = .red
    // dragDestination
    dragDestination.frame = CGRect(x: view.bounds.width/2 - itemSize/2, y: view.bounds.height - itemSize/2 - 200, width: itemSize, height: itemSize)
    dragDestination.backgroundColor = .blue
  }
}

extension ViewController: UIDragInteractionDelegate {
  func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
    let localObject = CustomClass(name: "me")
    let stringItemProvider = NSItemProvider(object: "Hello" as NSString)
    let item = UIDragItem(itemProvider: stringItemProvider)
    item.localObject = localObject
    return [
      item
    ]
  }
}

extension ViewController: UIDropInteractionDelegate {
  func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
    return true
  }
  
  func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
    let dropLocation = session.location(in: view)
    
  }
}

class CustomClass {

  var name: String
  
  init(name: String) {
    self.name = name
  }
  
}
カテゴリー
iOS Swift

UserDefaultsにデータを保存する

// to set data in UserDefaults
UserDefaults.standard.set(Float(0.0), forKey: "some_key")

// to fetch from UserDefaults
UserDefaults.standard.float(forKey: "some_key")
カテゴリー
iOS Swift

UINavigationControllerサンプルコード

globalにNavigationControllerを設定するにはSceneDelegate内で設定をします。

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    guard let windowScene = (scene as? UIWindowScene) else { return }
    window = UIWindow(frame: windowScene.coordinateSpace.bounds)
    window?.windowScene = windowScene
    let navigationController = UINavigationController(rootViewController: SequenceViewController())
    window?.rootViewController = navigationController
    window?.makeKeyAndVisible()
}

別のスクリーンへ遷移するには以下のようにします。

@objc private func goToVC() {
  let targetVC = NextViewController()
  navigationController?.pushViewController(targetVC, animated: true)
}

iOSのスクリーン遷移は、navigationController.viewControllersという配列にUIViewControllerをスタックする形で管理します。試しに、遷移先のviewDidLoad内でviewControllers配列をprintしてみます。その時点で表示されているViewControllerは配列内の一番最後のものだと分かります。

// print(navigationController?.viewControllers)

Optional([<ChordGenius.ViewController: 0x106817200>, <ChordGenius.ChordSelectVC: 0x105f07bd0>])

ひとつ前のViewControllerに戻るには、popViewController()を使います。なお、popToRootViewController()は一番最初のVCに戻る時に使います。

Transitionの挙動を設定するにはCATransitionを使い、navigationControllerに設定します。

@objc private func transitionButtonTapped() {
    let transition = CATransition()
    transition.duration = 0.5
    transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    transition.type = .fade
    navigationController?.view.layer.add(transition, forKey: nil)
    navigationController?.popToRootViewController(animated: false)
}

個別のviewController内に新しいnavigationControllerインスタンスを作成し、presentすることも出来ます。

@objc private func transitionButtonTapped() {
  let targetVC = BookmarkedViewController()
  let navigationVC = UINavigationController(rootViewController: targetVC)
  navigationVC.modalTransitionStyle = .crossDissolve
  navigationVC.modalPresentationStyle = .fullScreen
  self.present(navigationVC, animated: true)
}

viewDidLoad内でNavigationItemを設定するには以下のようにします。

// some basic examples
title = "Manage Songs"
navigationController?.navigationBar.prefersLargeTitles = true
    self.navigationItem.setRightBarButton(UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addTapped)), animated: true)

余談ですが、UINavigationControllerを設定するとview.safeAreaInsets.topの値が少し大きくなります。

カテゴリー
iOS Swift

SVGをUIButtonのアイコンとして使う

SVGをiOSで使う場合はまずAssetsフォルダにSVGを保存し、以下のように設定します。

こうすることで通常のSF Symbolのアイコンのように tintColorが反映されるようになります。

lazy var gotoSequenceButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(named: "play.square.stack"), for: .normal)
    button.imageView?.contentMode = .scaleAspectFit
    button.contentHorizontalAlignment = .fill
    button.contentVerticalAlignment = .fill
    button.tintColor = .white
    button.addTarget(self, action: #selector(onGotoSequenceButtonTapped), for: .touchUpInside)
    return button
}()