カテゴリー
AudioKit iOS Swift

AudioKitにSoundFontを読み込みシーケンスの最小構築

gitHub repo

SoundFontのBankが一つの場合は:

func loadMelodicSoundFont(url: URL, preset: Int, in bundle: Bundle = .main) throws {
  try loadSoundFont(url: url, preset: preset, type: kAUSampler_DefaultMelodicBankMSB, in: bundle)
}

Bankがいくつかある場合はtype: IntにBank番号をアサインして使う。

func loadSoundFont(_ file: String, preset: Int, bank: Int, in bundle: Bundle = .main) throws {
  guard let url = findFileURL(file, withExtension: ["sf2", "dls"], in: bundle) else {
  Log("Soundfont file not found: \(file)")
  throw NSError(domain: NSURLErrorDomain, code: NSFileReadUnknownError, userInfo: nil)
  }
  do {
    var bMSB: Int
    if bank <= 127 {
      bMSB = kAUSampler_DefaultMelodicBankMSB
  } else {
    bMSB = kAUSampler_DefaultPercussionBankMSB
  }
  let bLSB: Int = bank % 128
  try samplerUnit.loadSoundBankInstrument(
      at: url,
      program: MIDIByte(preset),
      bankMSB: MIDIByte(bMSB),
      bankLSB: MIDIByte(bLSB)
    )
  } catch let error as NSError {
    Log("Error loading SoundFont \(file)")
    throw error
  }
}
// setup example
private func setup() {
  // load sound font
  loadSF2(name: soundFonts[1], bank: 17, preset: 27, sampler: guitarSampler)
  
  // create a track if the mainSequencer doesn't have one
  if mainSequencer.trackCount < 1 {
    let _ = mainSequencer.newTrack("guitarTrack")
  }
  // add sampler to the mixer
  mixer.addInput(guitarSampler)
  // assign mixer as the engine's output
  engine.output = mixer
  do {
    try engine.start()
  } catch {
    print("error starting the engine: \(error)")
  }
}

public func loadSequence() {
  // time will be tracking the position of the cursor
  var time = 0.0
  // I don't need to set the playhead to zero here...
  // mainSequencer.setTime(0.0)
  let guitarTrackManager = mainSequencer.tracks[0]
  guitarTrackManager.clearRange(start: Duration(beats: time), duration: Duration(beats: 200))
  // adding a note
  guitarTrackManager.add(noteNumber: 50, velocity: 127, position: Duration(beats: time), duration: Duration(beats: 0.5))
  // then, increment the time
  time += 0.5
  // add another note
  guitarTrackManager.add(noteNumber: 52, velocity: 127, position: Duration(beats: time), duration: Duration(beats: 1))
  // connect track to the sampler's midi in
  guitarTrackManager.setMIDIOutput(guitarSampler.midiIn)
    
}
  
public func play() {
  loadSequence()
  mainSequencer.stop()
  // rewind the playhead to the very start
  mainSequencer.setTime(0.0)
  mainSequencer.setTempo(60)
  mainSequencer.play()
}

// in case you want to enable/disable looping  
public func toggleLoop() {
  if mainSequencer.loopEnabled {
    mainSequencer.disableLooping()
  } else {
    mainSequencer.enableLooping()
  }
}
カテゴリー
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
  }
  
}