カテゴリー
iOS Swift

ViewControllerごとにOrientationを指定したい

iOS15 参考

iOS16以降 参考

Helper

class Helper {
  static func forceRotate(orientation: UIInterfaceOrientationMask) {
    (UIApplication.shared.delegate as? AppDelegate)?.orientation = orientation
  let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene

  windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))

UIApplication.navigationTopViewController()?.setNeedsUpdateOfSupportedInterfaceOrientations()
  }
}

extension UIApplication {
  class func navigationTopViewController() -> UIViewController? {
    let nav = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController
    return nav?.topViewController
  }
}

appDelegate

var orientation: UIInterfaceOrientationMask = .portrait
  
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return orientation
}

viewDidLoad()

override func viewDidLoad() {
  super.viewDidLoad()

  Helper.forceRotate(orientation: .portrait)
}
カテゴリー
iOS Swift

Custom Slider Barを作ってみた

Github Repo

UIViewのoverride func touchesMoved() のtouchesは色々な情報を含んでいますが、その中のtouches: Set<UITouch>を利用しています。

ポイントとしては。。。

scaleBar.layer.anchorPointのイニシャル値はセンター(0.5, 0.5)ですが、(0.5, 1)とすることでshapeの底辺中央をanchorPointに設定できます。

iOSではy値はその自身の左上コーナーを基点とするので、自身のheightからy値を引いたものをyValueとし、scaleBy(x: 1, yValue)でy方向にのみscaleさせています。

0 <= yValue <= height としたいため、max(min(height, touch.location(in: self).y), 0) としています。

RxSwiftのBehaviorRelayを使って、スライダーの値が変化する度にUIなどを変更出来るようにしています。

lazy var scaleBar: UIView = {
    let scaleBar = UIView()
    scaleBar.translatesAutoresizingMaskIntoConstraints = false
    scaleBar.backgroundColor = .blue
    // initial anchorPoint is (0.5, 0.5) and the shape stretches from the center
    scaleBar.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
    return scaleBar
}()

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let yValue = height - max(min(height, touch.location(in: self).y), 0)
    yValueRelay.accept(yValue)
    scaleBar.transform = startTransform?.scaledBy(x: 1, y: yValue) ?? CGAffineTransform.identity
}
カテゴリー
iOS Swift

UITableViewを使ってカスタムPickerViewのようなものを作る

Sample Code

class ViewController: UIViewController {

  let dataCount = 10 // this will be replaced by the datasource count.
  
  var currentIndex: Int = 0 {
    didSet {
      print("aboveButton.isHidden: \(aboveButton.isHidden)")

      if currentIndex == 0 {
        UIView.animate(withDuration: 1, animations: {
          self.aboveButton.alpha = 0
        })
      } else if currentIndex == dataCount - 1 {
        UIView.animate(withDuration: 1, animations: {
          self.belowButton.alpha = 0
        })
      }
      else {
        UIView.animate(withDuration: 0.5, animations: {
          self.aboveButton.alpha = 1
          self.belowButton.alpha = 1
        })
      }
    }
  }
  
  let tableViewHeight: CGFloat = 300
  
  let buttonSize: CGFloat = 50
  
  lazy var aboveButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(systemName: "chevron.up"), for: .normal)
    button.imageView?.contentMode = .scaleAspectFit
    button.contentHorizontalAlignment = .fill
    button.contentVerticalAlignment = .fill
    button.tintColor = .systemGray
    button.addTarget(self, action: #selector(onAboveButtonTapped), for: .touchUpInside)
    return button
  }()
  
  @objc private func onAboveButtonTapped() {
    let index = currentIndex - 1
    guard index >= 0 else { return }
    let indexPath = IndexPath(row: index, section: 0)
    tableView.scrollToRow(at: indexPath, at: .top, animated: true)
    currentIndex = index
  }
  
  lazy var belowButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
    button.imageView?.contentMode = .scaleAspectFit
    button.contentHorizontalAlignment = .fill
    button.contentVerticalAlignment = .fill
    button.tintColor = .systemGray
    button.addTarget(self, action: #selector(onBelowButtonTapped), for: .touchUpInside)
    return button
  }()
  
  @objc private func onBelowButtonTapped() {
    let index = currentIndex + 1
    guard index <= dataCount - 1 else { return }
    let indexPath = IndexPath(row: index, section: 0)
    tableView.scrollToRow(at: indexPath, at: .top, animated: true)
    currentIndex = index
  }
  
  lazy var tableView: UITableView = {
    let tableView = UITableView()
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    tableView.separatorColor = .clear
    tableView.rowHeight = tableViewHeight
    return tableView
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    view.addSubview(tableView)
    view.addSubview(aboveButton)
    view.addSubview(belowButton)
    if currentIndex == 0 {
      aboveButton.alpha = 0
    }
    if currentIndex == dataCount - 1 {
      belowButton.alpha = 0
    }
    tableView.delegate = self
    tableView.dataSource = self
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    layout()
    
  }
  
  private func layout() {
    tableView.frame = CGRect(x: 0, y: view.bounds.height/2 - tableViewHeight/2, width: view.bounds.width, height: tableViewHeight)
    aboveButton.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: tableView.frame.minY + 10, width: buttonSize, height: buttonSize)
    belowButton.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: tableView.frame.minY + tableViewHeight - 10 - buttonSize, width: buttonSize, height: buttonSize)
  }

}

extension ViewController: UITableViewDelegate {
  
}

extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    dataCount
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    cell.textLabel?.text = "\(indexPath.row)"
    cell.backgroundColor = .systemGray6
    return cell
  }
  
}

extension ViewController: UIScrollViewDelegate {
  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    var visibleRect = CGRect()
    visibleRect.origin = tableView.contentOffset
    visibleRect.size = tableView.bounds.size
    let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
    guard let indexPath = tableView.indexPathForRow(at: visiblePoint) else { return }
    currentIndex = indexPath.row
    tableView.scrollToRow(at: indexPath, at: .top, animated: true)
  }
  
  func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                                 withVelocity velocity: CGPoint,
                                 targetContentOffset: UnsafeMutablePointer<CGPoint>)
  {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
      self.scrollViewDidEndDecelerating(scrollView)
    }
  }
}
カテゴリー
iOS Swift

UICollectionView Programmatically

Sample code

class ViewController: UIViewController {

  var viewWidth: CGFloat {
    return view.bounds.width
  }
  
  var viewHeight: CGFloat {
    return view.bounds.height
  }
  
  lazy var collectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
    layout.itemSize = CGSize(width: viewWidth*0.8, height: 200)
    let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
    collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
    collectionView.backgroundColor = .yellow
    
    return collectionView
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    view.addSubview(collectionView)
    collectionView.delegate = self
    collectionView.dataSource = self
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    
  }
  
  private func layout() {
    collectionView.frame = view.bounds
  }

}

extension ViewController: UICollectionViewDelegate {
}

extension ViewController: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 5
  }
  
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
    cell.backgroundColor = .blue
    return cell
  } 
}
// when you want access to the cells from your custom function

func doSomethinWithTheCell(index: Int) {
  guard let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return }
  cell.backgroundColor = .yellow
}
カテゴリー
iOS Swift

AudioKit パッケージを追加する

毎回手順を忘れて時間をロスするので笑 ここに記録しておきます。。。

File -> Add Packages
Click + button at the left bottom corner -> Add Package Collection
Enter JSON URL

Select the Project in the dropdown -> Add Package

カテゴリー
iOS RxSwift Swift

UIPickerViewサンプルコード

Github Repo

カテゴリー
iOS Swift

UIBezierPathをスケール(拡大・縮小)させる

情報元

// UIBezierPath for drawing an x-mark in a CAShapeLayer
private func getXMarkPath() -> CGPath {
  let path = UIBezierPath()
  // the origin of the path needs to be at 0, 0
  path.move(to: CGPoint(x: 0, y: 0))
  path.addLine(to: CGPoint(x: 50, y: 50))
  path.move(to: CGPoint(x: 50, y: 0))
  path.addLine(to: CGPoint(x: 0, y: 50))
  return path.cgPath
}
	
// UIBezierPath for drawing a circle in a CAShapeLayer
private func getCircleMarkPath() -> CGPath {
  // arcCenter and the radius should match
  let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 50, startAngle: 0, endAngle: CGFloat.pi*2, clockwise: true)
  return path.cgPath
}

private func resizePath(frame: CGRect, path: CGPath) -> CGPath {
  let boundingBox = path.boundingBox
  let boundingBoxAspectRatio = boundingBox.width / boundingBox.height
  let viewAspectRatio = frame.width / frame.height
  var scaleFactor: CGFloat = 1.0
  if (boundingBoxAspectRatio > viewAspectRatio) {
    // width is the limiting factor
    scaleFactor = frame.width / boundingBox.width
  } else {
    // height is the limiting factor
    scaleFactor = frame.height / boundingBox.height
  }
    var scaleTransform = CGAffineTransform.identity
    scaleTransform = scaleTransform.scaledBy(x: scaleFactor, y: scaleFactor)
    //	scaleTransform.translatedBy(x: -boundingBox.minX, y: -boundingBox.minY)
		
    // let scaledSize = boundingBox.size.applying(CGAffineTransform (scaleX: scaleFactor, y: scaleFactor))
    //	let centerOffset = CGSize(width: (frame.width - scaledSize.width ) / scaleFactor * 2.0, height: (frame.height - scaledSize.height) /  scaleFactor * 2.0 )
    //  scaleTransform = scaleTransform.translatedBy(x: centerOffset.width, y: centerOffset.height)
    //  CGPathCreateCopyByTransformingPath(path, &scaleTransform)
    let  scaledPath = path.copy(using: &scaleTransform)
		
    return scaledPath!
}

// assign the resized path to the CAShape layer added to the view's default layer
private func layout() {
  let newView = UIView()
  newView.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
  let xMarkPath = getXMarkPath()
  let resizedPath = resizePath(frame: newView.frame, path: xMarkPath)
  let shapeLayer = CAShapeLayer()
  shapeLayer.strokeColor = UIColor.black.cgColor
  shapeLayer.lineWidth = 1
  shapeLayer.fillColor = UIColor.blue.cgColor
  newView.layer.addSublayer(shapeLayer)
  shapeLayer.path = resizedPath
}
カテゴリー
iOS RxSwift Swift

RxSwiftとUITableViewを連携し、複数のSectionのTableViewを作成

RxSwiftとUITableViewの設定は少し分かりづらかったので、ここにまとめてみます。

Sample Code on github

まず、”SectionModel”というものを作成します。これは、UITableViewをいくつかのセクションに分ける場合の、いくつかのセクションと、そのセクション内のTableViewCellの内容に対応しています。

// ConfigViewSectionModelはSectionModel (RxDatasourcesで定義されているstruct)のtypealiasとして作成します。

typealias ConfigViewSectionModel = SectionModel<ConfigViewSection, ConfigItem>

// ConfigViewSectionはenumとして作成し、それぞれのセクションに応じて作成します。この例では"parameter", "playType", "interval"という3つのセクションに対応しています。
enum ConfigViewSection {
	
	// number of sections as needed ...
	case parameter
	case playType
	case interval
	
}

// ConfigItemもenumとして作成
enum ConfigItem {
	
	// parameter section
	case gameMode
	case tempo
	case timeLimit
	case tries
	case counts
	case lowest
	case highest
	case pianoVolume
	case metronomevolume
	
	// type section
	case ascend
	case descend
	case quarterNote
	case eighthNote
	case dyad
	
	// interval section
	case minSecond
	case second
	case minThird
	case third
	case fourth
	case dimFifth
	case fifth
	case minSixth
	case sixth
	case minSeventh
	case seventh
	case octave
}

ViewControllerでは以下のように設定

class ConfigViewController: UIViewController {

  lazy var tableView: UITableView = {
    let tableView = UITableView()
    // カスタムTableViewCellはいくつでもRegister可能 (Identifierで管理)
    tableView.register(ConfigParameterTVCell.self, forCellReuseIdentifier: ConfigParameterTVCell.identifier)
    tableView.register(ConfigSwitchTVCell.self, forCellReuseIdentifier: ConfigSwitchTVCell.identifier)
    // セパレーターを消したい場合は.clearを指定
    tableView.separatorColor = UIColor.clear
    // AutoLayoutのためこのパラメータをfalseにする
    tableView.translatesAutoresizingMaskIntoConstraints = false
    return tableView
  }()

  lazy var datasource = RxTableViewSectionedReloadDataSource<ConfigViewSectionModel>(configureCell: configureCell)
	
  lazy var configureCell: RxTableViewSectionedReloadDataSource<ConfigViewSectionModel>.ConfigureCell = { [weak self] (dataSource, tableView, indexPath, _) in
    // indexPathによりdataSourceからitem (enum型のConfigItem)をゲットする
    let item = dataSource[indexPath]
    // itemをswitchする
    switch item {
      case .tempo:
        // まず、事前にregisterしてあるCustomTableViewCell (この場合はConfigParameterTVCell) をdequeueする
        let cell = tableView.dequeueReusableCell(withIdentifier: ConfigParameterTVCell.identifier, for: indexPath) as! ConfigParameterTVCell
        // その後、cellの各パラメータに対して処理を記載
        cell.label.text = "something something"
        return cell
      case .timeLimit:
        // 同様にcellをdequeueし処理する

        // 以下省略
  }
}

RxSwiftのSubject (この場合はBehaviorRelay)とのDataBindingは以下のようにします。

private func setupViewModel() {
  configItems.asObservable()
    .bind(to: tableView.rx.items(dataSource: datasource))
    .disposed(by: bag)
}

上記のconfigItemsは以下のように定義し、設定します。

// configItemsは[ConfigViewSectionModel]のBehaviorRelayで、初期値は空の配列
let configItems = BehaviorRelay<[ConfigViewSectionModel]>(value: [])

// 3つのセクションに応じてそれぞれConfigViewSectionModelを作成します。
func updateConfigItems() {
  let sections: [ConfigViewSectionModel] = [
    parameterSection(),
    playTypeSection(),
    intervalSection(),
  ]
  // configItemsに[ConfigViewSectionModel]を渡します
  configItems.accept(sections)
}

// parameterSectionの実装
func parameterSection() -> ConfigViewSectionModel {
  let items: [ConfigItem] = [
    .gameMode,
    .tempo,
    .timeLimit,
    .tries,
    .counts,
    .lowest,
    .highest,
    .pianoVolume,
    .metronomevolume,
  ]
  // このSectionModelのConstructorはRxDatasoucesで定義されている
  return ConfigViewSectionModel(model: .parameter, items: items)
}

// playTypeSectionの実装
func playTypeSection() -> ConfigViewSectionModel {
  let items: [ConfigItem] = [
    .ascend,
    .descend,
    .quarterNote,
    .eighthNote,
    .dyad,
  ]
  // このSectionModelのConstructorはRxDatasoucesで定義されている
  return ConfigViewSectionModel(model: .playType, items: items)
}

// intervalSectionの実装
func intervalSection() -> ConfigViewSectionModel {
  let items: [ConfigItem] = [
    .minSecond,
    .second,
    .minThird,
    .third,
    .fourth,
    .dimFifth,
    .fifth,
    .minSixth,
    .sixth,
    .minSeventh,
    .seventh,
    .octave,
  ]
  // このSectionModelのConstructorはRxDatasoucesで定義されている
  return ConfigViewSectionModel(model: .interval, items: items)
}
カテゴリー
iOS Swift

UITableViewを学ぶ

UITableViewはポイントを抑えさえすればとても簡単です。

class ViewController: UIViewController {
  // create an instance of UITableView in a lazy fashion
  lazy var tableView: UITableView = {
    let tableView = UITableView()
    return tableView
  }()
  // custom class for delegate and datasources
  let customClass = CustomClass.shared

  override func viewDidLoad() {
    super.viewDidLoad()
    // set a background color so the view is visible
    view.backgroundColor = .white
    // add the tableView to the view
    view.addSubview(tableView)
    // set the frame of the tableView to view.bounds
    tableView.frame = view.bounds
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "canBeAnythingUnique")
    // you can extend ViewController as the delegate and datasouces, or you can create a class for it.
    tableView.delegate = customClass
    tableView.dataSource = customClass
  }
}

// a custom class to handle datasources and delegate methods
class CustomClass: NSObject {
  // make this class singleton
  public static let shared = CustomClass()
  private override init() {
    super.init()
  }
}

// extend the CustomClass as UITableViewDatasource
extension CustomClass: UITableViewDataSource {
  // two required methods
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // in real world scenario, you would return the count of a data array
    return 10
  }
	
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    // a standard UITableViewCell has a textLabel as default
    cell.textLabel?.text = "Hello World!!"
    return cell
  }
}

extension CustomClass: UITableViewDelegate {
  // any delegation methods goes here...	
}

カスタムセルを作る場合は、UITableViewCellをエクステンドします。

class CustomTableViewCell: UITableViewCell {

  static let identifier = "CustomTableViewCell"
	
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
		
    contentView.backgroundColor = .orange
  }
	
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }	
}
カテゴリー
iOS Swift

JSONをパースし、オブジェクトとして使用する

Web APIから入手したり、もしくはBundleに保存したJSONファイルをパースし、オブジェクトとして使用出来るようにする。

例えば以下のようなJSONがあったとして、、、

{
  "chords": [
    {
      "quality": "Major 7th",
      "voicing": "Drop 2 & 4",
      "inversion": "Root Position",
      "notes": "36,16,31,12",
      "root_note": 36
    },
    {
      "quality": "Major 7th",
      "voicing": "Drop 2 & 4",
      "inversion": "1st Inversion",
      "notes": "16,11,37,33",
      "root_note": 11
     }
  ]
}

この場合、以下のように対応するstructを作り

struct ServerResponse: Codable {
	let chords: [ResponseChord]
	
	struct ResponseChord: Codable, Hashable {
		let quality: String
		let voicing: String
		let inversion: String
		let notes: String
		let root_note: Int
	}
}

以下のように使用します。

guard let jsonPath = Bundle.main.path(forResource: "chords", ofType: "json") else { return }
do {
  let data = try Data(contentsOf: URL(fileURLWithPath: jsonPath))
  let jsonResult = try JSONDecoder().decode(ServerResponse.self, from: data)
  print(jsonResult)
  } catch {
			
}