SightReading(视奏)应用开发知识点总结

数据持久化

文件的保存和读取

  • Json文件:每个乐谱页对应一个Json文件。文件保存乐谱每小节的大小和位置信息,以及乐谱的基本信息,包括每小节节拍数和用户设置的速度和Mask偏移量。
  • png文件:每个乐谱都有一个乐谱图片,如果用户做了笔记,还有一个笔记图片。

保存文件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func getRootPath() -> String? {
    return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
}

func saveImageFile() {
    if let rootPath = Utility.getRootPath(),
        let imageName = getFileName() {
        let imagePath = "\(rootPath)/\(imageName).png"
        print("image path: \(imagePath)")
        if let image = imageView.image, let imageData = image.pngData() {
            FileManager.default.createFile(atPath: imagePath, contents: imageData, attributes: nil)
        }
    }
}

func saveJsonFile() {
    if let rootPath = Utility.getRootPath(),
        let jsonFileName = getFileName() {
        let jsonPath = "\(rootPath)/\(jsonFileName).json"
        print("image path: \(jsonPath)")
        let jsonDic: [String: Any] = [basicInfoKey: [String: String](), barFramesKey: barFrames]
        if let jsonData = try? NSKeyedArchiver.archivedData(withRootObject: jsonDic, requiringSecureCoding: false) {
            FileManager.default.createFile(atPath: jsonPath, contents: jsonData, attributes: nil)
        }
    }
}

读取文件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func loadJsonFile() {
    if let rootPath = Utility.getRootPath(),
        let jsonName = navigationItem.title,
        let jsonData = FileManager.default.contents(atPath: "\(rootPath)/\(jsonName).json"),
        let jsonObjectAny = NSKeyedUnarchiver.unarchiveObject(with: jsonData),
        let jsonObject = jsonObjectAny as? [String: Any] {
        if let sheetBasicInfo = jsonObject[basicInfoKey] as? [String: String] {
            self.sheetBasicInfo = sheetBasicInfo
        }
        if let barFrames = jsonObject[barFramesKey] as? [Int: CGRect] {
            self.barFrames =  barFrames
        }
    }
}

func loadSheetImage(with imageName: String) {
    if let rootPath = Utility.getRootPath(),
        let sheetImage = UIImage(contentsOfFile: "\(rootPath)/\(imageName).png") {
        sheetImageView.image = sheetImage
        noteImageView.image = UIImage(contentsOfFile: "\(rootPath)/\(imageName)\(noteImageSubfix).png")
        layoutImageView()
    }
}

NSDefault:保存标签信息

1
2
UserDefaults.standard.setValue(allTags, forKey: allTagsKey)
let allTags = UserDefaults.standard.value(forKey: allTagsKey)

参考

播放节拍器声音

通过AVFoundation播放声音

1
2
3
4
if let audioUrl = Bundle.main.url(forResource: "FirstMeter", withExtension: "wav", subdirectory: "Resource.bundle")  {
    AudioServicesCreateSystemSoundID(audioUrl as CFURL, &firstMeterId)
}
AudioServicesPlaySystemSound(self.firstMeterId)

节拍器音量调节

在设置中,如果“声音->铃声和警报”下面的“跟随按钮”没有打开,那么通过AudioServicesPlaySystemSound()播放的声音就会始终用一个固定的音量播放,其它声音API播放的声音(比如AVAudioPlayer)会跟随系统音量变化音量大小。

In Settings app, Sounds->RINGER AND ALERTS, if ‘Change with Buttons’ is set to Off, then sounds using AudioServicesPlaySystemSound() will always be played at a fixed volume (yet other sound API’s such as AVAudioPlayer will respect the volume of the device).

navigationController的使用和数据的传递

通过下面两种方法进行ViewController的弹出

1
2
3
let playVC = storyBoard.instantiateViewController(identifier: "Play")
playVC.navigationItem.title = filtedFileNames[indexPath.row]
navigationController?.pushViewController(playVC, animated: true)

或者

1
2
3
4
let colorPickerVC = UIColorPickerViewController()
colorPickerVC.selectedColor = brushColorButton.selectedColor
colorPickerVC.delegate = self
present(colorPickerVC, animated: true, completion: nil)

通过delegate进行目标ViewController到源ViewController的方法调用和数据传递,源ViewControler可以直接设置目标ViewController的属性进行数据传递。

参考

PhotoKit的使用

需要在info.plist里面设置NSPhotoLibraryUsageDescription属性,设置在获取权限的时候显示给用户的弹窗中显示的内容。

检查权限状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private func requestPrivilegeAndLoadPhotos() {
    let status = PHPhotoLibrary.authorizationStatus()
    if status == .authorized {
        loadPhotos()
    } else {
        PHPhotoLibrary.requestAuthorization { (status) in
            if status == .authorized {
                self.loadPhotos()
                DispatchQueue.main.async {
                    self.collection.reloadData()
                }
            } else {
                // use not grant the privilege
            }
        }
    }
}

如果用户给了访问相册的权限,通过下面的方法加载所有图片信息:

1
2
3
4
5
private func loadPhotos() {
    let allPhotosOptions = PHFetchOptions()
    allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
    allPhotos = PHAsset.fetchAssets(with: .image, options: allPhotosOptions)
}

通过下面方法将所有图片信息显示在collection列表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return allPhotos.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    if let cell = collection.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? PhotoCollectionCell {
        let assert = allPhotos.object(at: indexPath.item)
        PHImageManager.default().requestImage(for: assert, targetSize: CGSize(width: photoCollectionWH, height: photoCollectionWH), contentMode: .aspectFill, options: .none) { (image, dic) in
            if let image = image {
                cell.imageView.image = image
            }
        }

        return cell
    } else {
        return collection.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
    }
}

通过下面方法,将用户选择的图片传给delegate处理(delegate可以是源ViewController):

1
2
3
4
5
6
7
8
9
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    print("item: \(indexPath.item)")
    let assert = allPhotos.object(at: indexPath.item)
    PHImageManager.default().requestImage(for: assert, targetSize: CGSize(width: assert.pixelWidth, height: assert.pixelHeight), contentMode: .aspectFill, options: .none) { (image, dic) in
        if let image = image {
            self.delegate?.set(image: image, and: nil)
        }
    }
}

参考

Alert View

Displaying Alerts with UIAlertController in Swift

UIImage

下面两个图片加载方法对cache的运用是不一样的:

1
2
+ (UIImage *)imageNamed:(NSString *)name: use cached images
+ (UIImage *)imageWithContentsOfFile:(NSString *)path: skip cached images and read data directly from file

参考

iOS UIImage Cache

可编辑的UITableView

通过实现下面方法保证每个cell支持左滑操作:

1
2
3
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
}

通过实现下面的方法,左滑之后显示两个按钮: Edit Tags和Delete

1
2
3
4
5
6
7
8
9
10
11
12
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
    let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
        self.deleteItem(at: indexPath)
    }
    let editAction = UITableViewRowAction(style: .default, title: "Edit Tags") { (action, indexPath) in
        self.editTags(for: indexPath)
    }
    editAction.backgroundColor = UIColor(displayP3Red: 60/255, green: 148/255, blue: 1.0, alpha: 1.0)
    deleteAction.backgroundColor = .red

    return [deleteAction, editAction]
}

CALayer & CAShapeLayer & Core Graphics

这一块的内容太多,希望有时间可以单独总结一下。

音乐术语英语

音乐术语英文名称汇总

swift中构造方法designated,convenience

官方文档中有如下描述:

  1. 子类designated构造方法中必须调用父类的designated构造方法。
  2. convenience构造方法中必须调用当前类的构造方法。
  3. convenience构造方法归根结底要调用到designated构造方法。

swift init methods

参考

笔记绘制功能

通过第三方库STSketchKit实现笔记功能支持Undo/Redo操作。官方介绍如下:

ATSketchKit is a drawing / sketching framework for iOS written in Swift.

It can be used as the foundation for an artistic app, a simple signature feature or more inteligent graph designing app.