[Swift] FileManager 사용해보기
⛔️ FileManager에 대해 개인적으로 공부한 것을 정리한 글입니다. 최대한 올바른 내용만 적기위해 노력하고 있지만 틀린내용이 있을 수 있습니다. 그렇기 때문에 글을 읽으실때 비판적으로 읽어주세요.
틀린내용에 대한 피드백은 메일로 보내주시면 감사하겠습니다🙏🏻
1️⃣ FileManager 인스턴스 생성하기
FileManager는 다음과 같이 .default를 이용해 싱글턴인스턴스 혹은 기본인스턴스로 생성할 수 있습니다.
let singleton = FileManager.default // 싱글턴
let instance = FileManager() // 기본생성
내부구현을 보면 기본생성을
하지만 아래의 Apple문서에서는 shared(싱글턴)을 사용하면 쓰레드안전하게 사용이 가능하다고 말하는데, 다른말로 기본인스턴스생성으로 사용하면
2️⃣ FileManager를 이용해서 파일을 저장할 위치 정하기
파일을 관리할 폴더의주소를 지정해줄 수 있습니다. 애플이 지정해준 폴더를 사용하기 위해 FileManager클래스 내부에 구현된 .url(for:,in:)메서드를 이용해 폴더의 주소를 URL형태로 얻어오겠습니다.
let documentPath: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
첫번째 인자로 FileManager.SearchPathDirectory를 받아오는데, enum타입으로 여러종류의 폴더경로가 있습니다.
두번째 인자로 FileManager.SearchPathDomainMask을 받아오는데, 이것 역시 enum타입으로 user, local, network, system 도메인을 보안상 가려주는(?)마스크 역할을 하는 것 같습니다. (이부분은 좀 더 알아봐야할 것 같습니다.)
아래의 FileManager의 내부구현을 살펴보면
다음과 같이 4가지종류의 도메인마스크를 사용하여 URL을 만들어서 출력해 보았습니다.
private let useUserMask: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
private let useSystemMask: URL = FileManager.default.urls(for: .printerDescriptionDirectory, in: .systemDomainMask)[0]
private let useLocalMask: URL = FileManager.default.urls(for: .developerDirectory, in: .localDomainMask)[0]
private let useNetworkMask: URL = FileManager.default.urls(for: .demoApplicationDirectory, in: .networkDomainMask)[0]
인덱스 0 접근?
다시 이번에 사용할 URL주소를 확인해보면 배열의
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
FileManager의 내부구현을 살펴보면 아래의 두가지 케이스때문에 배열형태로 반환해준다는 것을 확인할 수 있습니다. 아래의 두가지 케이스를 제외하고 나머지 모든 케이스는 성공시 배열에 URL주소를 1개만넣어 반환 해줍니다.
private let documentPath: URL? =
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[safe: 0]
3️⃣ Create, Read, Delete 메서드 구현하기
Create(파일 저장) 메서드 구현하기
이번 포스트에서는 이미지파일을 저장하는 것을 다룰예정입니다. 결과적으로 다음과 같이 Create메서드를 구현했습니다.
Data타입클래스에 있는 .write(to:)메서드를 이용하면 파일을 해당위치에 저장할 수 있습니다.
좀 더 디테일하게 저장하고 싶으면 NSData.WritingOptions옵션이 포함된 .write(to:options:)메서드를 이용하면 됩니다.
func saveImage(filename: String, image: UIImage?) -> String? {
guard let image = image else {
return nil
}
guard let imageData = image.jpegData(compressionQuality: 1) ?? image.pngData() else { return nil }
do {
guard let documentPath = documentPath else {
throw URLError(.badURL)
}
try imageData.write(to: documentPath.appendingPathComponent(filename + "png"))
let locationString = filename + "png"
return locationString
} catch {
print(error)
return nil
}
}
UIImage에는 jpeg 혹은 png파일로 변환해주는 메서드가 있는데, 이 메서드들을 이용해서 파일형태로 만든 뒤 저장해주면 됩니다.
jpeg확장자와 png확장자에 대해서는 이 링크를 읽어보시면될 것 같습니다. 맥에서 실험해본 결과 jpeg와 png파일간은 확장자만을 바꿔서 적어줘도 파일손상과 용량변화없이 변경이 잘 되는 것 같습니다.
단 위의 코드에서도 확인할 수 있듯이 jpeg로 변활할때는 0.0(퀄리티최소) ~ 1.0(퀄리티최대)를 지정해서 변환시켜줄 수 있습니다.
.jpegData(compressionQuality: 1) // 0.0 ~ 1.0
폴더생성하는 방법?
다음과 같이 FileManager클래스에 있는 .createDirectory(atPath,withIntermediateDirectories:attributes:)를 이용해서 폴더를 생성해줄 수도 있습니다. 파라미터를 간단히 살펴보면 다음과 같습니다.
- atPath: 경로
- withIntermediateDirectories: true이면 적어준 경로에서 상위경로가 존재하지 않으면 자동으로 디렉토리로 만들어줌
- attributes:딕셔너리형태로 소유자, 그룹번호, 파일권한, 수정날짜를 설정할 수 있음
try fileManager.createDirectory(atPath: filePath.path, withIntermediateDirectories: true, attributes: nil)
Read(파일 읽기) 메서드 구현하기
결과적으로 다음과 같이 Read메서드를 구현했습니다. UIImage(contentsOfFile:)로 확장자가 포함된 String경로를 넣어 이미지를 반환해주도록 했습니다.
func readImage(named: String) -> UIImage? {
guard let documentPath = documentPath else {
return nil
}
let result = UIImage(contentsOfFile: URL(fileURLWithPath: documentPath.absoluteString).appendingPathComponent(named).path)
return result
}
주의할점은 doucumentPath.absoluteString가 String타입인 점을 이용하여 그대로 사용하면 안됩니다.
다음과 같이 file://가 추가되어 있기 때문에 잘못된 경로를 넘겨주게 됩니다.
URL(fileURLWithPath: documentPath.absoluteString).appendingPathComponent(named).path // /User/xxx/xxx.png
documentPath.absoluteString + named // file:///User/xxx/xxx.png
Delete(파일 삭제) 메서드 구현하기
결과적으로 다음과 같이 Delete메서드를 구현했습니다.
func deleteImage(at filename: String) throws {
do {
guard let documentPath = documentPath else {
throw URLError(.badURL)
}
let urlString = documentPath.absoluteString + filename
let url = URL(string: urlString)!
try fileManager.removeItem(at: url)
} catch {
throw DBManagerError.failToRemoveImageFile
}
}
.removeItem()메서드의 경우 경로의 타입으로 String, Url 모두 사용이 가능합니다. 하지만 위에서도 언급했듯이 doucumentPath.absoluteString값을 String경로로써 그대로 사용할 경우 file://가 앞에 추가될 수 있다는점을 주의해야 합니다.
4️⃣ FileManager사용시 주의사항
User 도메인마스크 사용시 주의사항
이번 주의사항은 다음의 상황이 (모바일에서 앱을 업데이트 = 시뮬레이터 재시작)하는 것과 같이 동작한다면 위험할 수도 있는 부분입니다.
4가지 도메인마스크를 사용하여 주소에 접근하여 사용할 수 있습니다. 그중에서 시뮬레이터를 재시작할때마다
let documentPath: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let documentPath2: URL = FileManager.default.urls(for: .preferencePanesDirectory, in: .userDomainMask)[0]
let documentPath3: URL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)[0]
let documentPath4: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
아래이미지의 녹색부분이 시뮬레이터를 재실행할때마다 변경되었습니다. (백그라운드나 종료할때는 그대로)
기존 주소
시뮬레이터 재시작 후
다행히 폴더가 삭제된 뒤 새로 만들어지는 것이 아닌
let documentPath: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
func ...() {
let urlString = documentPath.absoluteString + filename
savePathToCoreData(path: documentPath.absoluteString + filename) // ❌
savePathToCoreData(path: filename) // ✅
}
결론적으로 주의할점은 파일의 이름, 경로등등.. 데이터를 CoreData형태로 저장할 때 “