[Swift] FileManager 사용해보기
⛔️ FileManager에 대해 개인적으로 공부한 것을 정리한 글입니다. 최대한 올바른 내용만 적기위해 노력하고 있지만 틀린내용이 있을 수 있습니다. 그렇기 때문에 글을 읽으실때 비판적으로 읽어주세요.
틀린내용에 대한 피드백은 메일로 보내주시면 감사하겠습니다🙏🏻
1️⃣ FileManager 인스턴스 생성하기
FileManager는 다음과 같이 .default를 이용해 싱글턴인스턴스 혹은 기본인스턴스로 생성할 수 있습니다.
let singleton = FileManager.default // 싱글턴
let instance = FileManager() // 기본생성
내부구현을 보면 기본생성을
하지만 아래의 Apple문서에서는 shared(싱글턴)을 사용하면 쓰레드안전하게 사용이 가능하다고 말하는데, 다른말로 기본인스턴스생성으로 사용하면 일단은 delegate를 사용하지 않을 것이기 때문에 싱글턴 인스턴스로 생성할 예정입니다. 그리고 앞으로 구현할 파일의 CRUD메서드를 담고있는 클래스 또한 싱글턴으로 만들어주는 것이 좋습니다. 그 이유는
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형태로 저장할 때 “