애플은 앱마다 각각의 저장 공간을 갖게하여 다른 앱과의 충돌을 방지시킨다. 이것은 A라는 앱은 A 앱에 할당된 파일들과 디렉터리에 접근할 수 있는 것이다, 사용자가 앱을 설치하였을 때 시스템은 표준 디렉터리들을 만들고 그 디렉터리에 파일들을 저장한다.
가장 유용한 디렉터리는 document directory다. document directory는 사용자의 파일과 파일 지원 디렉터리를 저장할 수 있는 곳이다.
디렉터리의 위치는 보장되지 않기 때문에 항상 시스템한테 현재 디렉터리나 파일을 가르키는 URL을 물어봐야한다. FileManager 클래스는 urls() 메서드를 가지고 있으며 2가지 인자값을 받는다. 다음과 같다.
// 사용자의 문서 경로
let documentPath: URL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
for: 폴더를 정해주는 요소. Download 혹은 Document 등등…
in: 제한을 걸어주는 요소. 그 이상은 못가게 하는…
for enum 값들
documentDirectory // documents (Documents)
developerDirectory // (Developer) DEPRECATED - there is no one single Developer directory.
desktopDirectory // location of user's desktop
downloadsDirectory // location of the user's "Downloads" directory
musicDirectory // location of user's Music directory (~/Music)
...
in enum 값들
public static var localDomainMask: FileManager.SearchPathDomainMask { get }
// local to the current machine --- place to install items available to everyone on this machine (/Library)
public static var networkDomainMask: FileManager.SearchPathDomainMask { get }
// publically available location in the local area network --- place to install items available on the network (/Network)
public static var systemDomainMask: FileManager.SearchPathDomainMask { get }
// provided by Apple, unmodifiable (/System)
public static var allDomainsMask: FileManager.SearchPathDomainMask { get }
// all domains: all of the above and future items
다음은 파일을 저장하는 코드다. FileManager를 manager라는 싱글톤 객체로 만들고 메니져의 urls 메서드를 만들어 documentDirectory에 접근할 수 있게 한다. urls 메서드는 디렉토리의 가능한 모든 위치를 가진 옵셔널 URL 구조의 배열을 반환합니다. documentDirectory 같은 경우 정확한 값은 배열에 첫 번째 값이므로 documents.first!를 사용한다. 두 번째로 userDomainMask는 Domain을 나타내는 파라미터로 유저의 데이터를 저장할 유저의 홈 디렉터리를 얻기 위해서 사용됩니다. 이후 appdendingPathComponent 메서드를 사용하여 name 이름을 URL 마지막에 추가해준다. 다음으로 path에 newFileURL.path 를 사용하여 새롭게 만들어진 파일 url을 담아 FileManager 를 통해서 파일을 생성한다. 만약 파일이 경로에 이미 존재한다면 덮어 쓴다.
import SwiftUI
class ApplicationData: ObservableObject {
func saveFile(name: String) {
let manager = FileManager.default
let documents = manager.urls(for: .documentDirectory, in: .userDomainMask)
let docURL = documents.first!
let newFileURL = docURL.appendingPathComponent(name)
let path = newFileURL.path
manager.createFile(atPath: path, contents: nil, attributes: nil)
}
}
실제로 파일을 만드는 작업을 수행해보면 아래와 같은 위치에서 tmp라는 파일이 생성되었음을 확인할 수 있다.
다음으로 ApplicationData 의 init() 함수를 만들어 처음 인스턴스가 생성되었을 때 사용자의 데이터가 파일이 저장될 URL을 설정하고 방금 설정한 URL에 기존에 저장된 파일을 가져와 listOfFiles에 파일을 추가하는 코드입니다.
- for 에 들어가는 값은 FileManager.SearchPathDirectory 로 파일을 찾을 디렉토리를 지정해줄 수 있습니다. 다운로드 폴더, 라이브러리 폴더 등 다양한 폴더를 지정해줄 수 있습니다.
- in 에 들어가는 값은 FileManager.SearchPathDomainMask 로 파일을 찾을 시스템 도메인을 지정해줄 수 있습니다.
saveFile 함수는 FileManager를 통해서 인자로 받은 name을 저장경로 뒤에 붙여 새로운 파일을 생성합니다.(기존에 있었으면 덮어씀) 이후 listOfFiles에 파일이 존재하는지 확인 후 만약 존재하지 않는 파일이라면 listOfFiles에 추가합니다.
struct File: Identifiable {
let id: UUID = UUID()
var name: String
}
class ApplicationData: ObservableObject {
@Published var listOfFiles: [File] = []
var manager: FileManager
var docURL: URL
init() {
manager = FileManager.default
let documents = manager.urls(for: .documentDirectory, in: .userDomainMask)
docURL = documents.first!
if let list = try? manager.contentsOfDirectory(atPath: docURL.path) {
for name in list {
let newFile = File(name: name)
listOfFiles.append(newFile)
}
}
}
func saveFile(name: String) {
let newFileURL = docURL.appendingPathComponent(name)
let path = newFileURL.path
manager.createFile(atPath: path, contents: nil, attributes: nil)
if !listOfFiles.contains(where: { $0.name == name}) {
listOfFiles.append(File(name: name))
}
}
}
디렉터리 만들기
파일 뿐만 아니라 같은 방법으로 디렉터리를 만들수도 있습니다. 디렉터리를 만드는 방법은 createDirectory() 함수를 사용하여 만들 수 있습니다. createDirectory() 함수는 3가지 인자를 받는데 각 인자는 디렉터리의 이름을 포함한 새로운 디렉터리의 경로, 존재하지 않는 경로인 경우 해당 경로를 만들것인지에 대한 여부 bool 값, 그리고 디렉터리의 인자값들입니다.
존재하지 않는 경로인 경우 해당 경로를 만들 것인지에 대한 여부를 사용하기 위해서는 error를 throw할 수 있기 때문에 try를 사용해야합니다.
createDirectory 메서드를 사용하여 original 과 archived 디렉터리에 만들어 파일을 저장할 수 있습니다. original 폴더는 유저로부터 만들어진 파일들을 포함하고 있고 archived 폴더는 유저가 기록 하기로 결정한 파일을 저장합니다. (다른 이름으로도 만들 수 있습니다)
original 디렉터리와 archived 디렉터리에 저장된 파일은 가져왔을때 listOfFiles 과 같은 형태로 저장될 수 있습니다. 작업중인 디렉터리를 구분하기 위해서 @Published currentDirectory를 사용합니다. ApplicationData 의 init() 함수를 만들어 처음 인스턴스가 생성되었을 때 사용자의 데이터가 파일이 저장될 URL을 설정합니다. 이후 for 문으로 directories 를 enumerated() 메서드를 사용하여 index 값에 original, archived 를 받고 directory에 0과 0을 받아서 새로운 디렉터리URL을 만들고 해당 디렉터리를 만든 이후 새롭게 만든 디렉터리에 있는 파일들을 가져오는 for 문을 만듭니다.
struct File: Identifiable {
let id: UUID = UUID()
var name: String
}
class ApplicationData: ObservableObject {
@Published var listOfFiles: [Int:[File]] = [:]
@Published var currentDirectory: Int
var manager: FileManager
var docURL: URL
let directories = ["original", "archived"]
init() {
listOfFiles = [0: [], 1: []] //디렉터리에서 가져온 파일들을 담는 프로퍼티 래퍼
currentDirectory = 0 //현재 설정된 디렉터리
manager = FileManager.default
docURL = manager.urls(for: .documentDirectory, in: .userDomainMask).first!
// (original, 0), (archived, 0)
for (index, directory) in directories.enumerated() {
let newDirectoryURL = docURL.appendingPathComponent(directory)
let path = newDirectoryURL.path
do {
//만일 기존에 없는 디렉터리일 경우 디렉터리를 생성하기
try manager.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
} catch {
print("The directory already exists")
}
//origianl과 archived에 저장된 파일이 있을 경우 가져오기
if let list = try? manager.contentsOfDirectory(atPath: path) {
for name in list {
let newFile = File(name: name)
listOfFiles[index]?.append(newFile)
}
}
}
}
func saveFile(name: String) {
let newFileURL = docURL.appendingPathComponent("\(directories[0])/\(name)")
let path = newFileURL.path
manager.createFile(atPath: path, contents: nil, attributes: nil)
if let exists = listOfFiles[0]?.contains(where: { $0.name == name }) {
if !exists {
let newFile = File(name: name)
listOfFiles[0]?.append(newFile)
}
}
}
}
다음으로는 파일을 삭제하는 기능을 추가합니다. fileURL에 삭제할 파일의 위치를 지정한 후 FileManager의 removeItem 메서드를 통해 파일을 삭제할 수 있습니다. 만일 파일 삭제에 성공하게 된다면 디렉터리에 있는 파일을 삭제합니다.
func deleteFile(name: String) {
let fileURL = docURL.appendingPathComponent("\(directories[currentDirectory])/\(name)")
do {
try manager.removeItem(atPath: fileURL.path)
listOfFiles[currentDirectory]?.removeAll(where: { $0.name == name} )
} catch {
print("File was not removed")
}
}
다음은 파일을 다른 디렉터리로 옮기는 기능을 추가합니다. origin에서 archived 디렉터리로 옮기는 코드로 FileManager의moveItem 메서드를 사용하여 atPath 에서 toPath 로 파일을 옮김니다.
func moveToArchived(name: String) {
let origin = docURL.appendingPathComponent("\(directories[0])/\(name)")
let destination = docURL.appendingPathComponent("\(directories[1])/\(name)")
if !manager.fileExists(atPath: destination.path) {
do {
try manager.moveItem(atPath: origin.path, toPath: destination.path)
listOfFiles[0]?.removeAll(where: { $0.name == name} )
listOfFiles[1]?.append(File(name: name))
} catch {
print("File was not moved")
}
}
}
그리고 이후 다음과 같은 UI를 추가할 수 있습니다.
import SwiftUI
struct ContentView: View {
@EnvironmentObject var appData: ApplicationData
@State private var openSheet: Bool = false
var body: some View {
NavigationStack {
VStack {
Picker("", selection: $appData.currentDirectory) {
ForEach(0..<appData.directories.count, id: \.self) { index in
Text(appData.directories[index]).tag(index)
}
}.pickerStyle(.segmented)
List {
ForEach(appData.listOfFiles[appData.currentDirectory] ?? []) { file in
RowFile(file: file)
}
}.listStyle(.plain)
}
.navigationBarTitle("Files")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add File") {
openSheet = true
}.disabled(appData.currentDirectory != 0 ? true : false)
}
}
.sheet(isPresented: $openSheet) {
AddFileView()
}
}
}
}
struct RowFile: View {
@EnvironmentObject var appData: ApplicationData
let file: File
var body: some View {
HStack {
Text(file.name)
Spacer()
if appData.currentDirectory == 0 {
Button(action: {
appData.moveToArchived(name: file.name)
}, label: {
Image(systemName: "folder")
.font(.body)
.foregroundColor(Color.green)
}).buttonStyle(.plain)
}
Button(action: {
appData.deleteFile(name: file.name)
}, label: {
Image(systemName: "trash")
.font(.body)
.foregroundColor(Color.red)
}).buttonStyle(.plain)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(ApplicationData())
}
}
import SwiftUI
struct AddFileView: View {
@EnvironmentObject var appData: ApplicationData
@Environment(\.dismiss) var dismiss
@State private var nameInput: String = ""
var body: some View {
VStack {
HStack {
Text("Name:")
TextField("Insert File Name", text: $nameInput)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)
.disableAutocorrection(true)
}.padding(.top, 25)
HStack {
Spacer()
Button("Create") {
var fileName = nameInput.trimmingCharacters(in: .whitespaces)
if !fileName.isEmpty {
fileName += ".txt"
appData.saveFile(name: fileName)
dismiss()
}
}
}
Spacer()
}.padding()
}
}
struct AddFileView_Previews: PreviewProvider {
static var previews: some View {
AddFileView().environmentObject(ApplicationData())
}
}
파일 속성값 읽기
어떤 애플리케이션들은 파일 이름 뿐만 아니라 더 상세한 정보를 알아야할수도 있습니다. FileManager 클래스는 파일의 속성을 가져오는 메서드를 제공합니다. 여기서 말하는 속성이란 파일이 만들어진 날짜 또는 파일의 크기 등이 있습니다. 메서드는 딕셔너리로 값을 가져오며 key로 사용할 수 있는 미리 만들어진 상수들이 존재합니다. 주로 사용되는 것이 creationdate,modificationdate,size,type입니다.
다음 메서드는 선택한 파일의 속성값을 어떻게 읽어오는지 보여줌니다. file 인자는 UUID로 받습니다. listOfFiles에서 UUID로 값을 찾고 해당 파일을 디렉터리에서 찾을 수 있는 fileURL를 생성합니다. fileURL 경로를 통해서 만약 존재하는 파일이라면 attributes를 가져올 수 있는 FileManager의 attributesOfItem 메서드를 사용합니다. 가져온 속성값들은 any 타입으로 되어있기 때문에 타입 캐스팅을 해야합니다. if 문에서 만약 파일의 타입이 디렉터리가 아니라면 해당 파일이름, 확장자, 크기, 파일생성 날짜를 반환합니다.
func getDetails(file: UUID) -> (String, String, String, String) {
var values = ("", "", "", "")
if let file = listOfFiles[currentDirectory]?.first(where: { $0.id == file }) {
let fileURL = docURL.appendingPathComponent("\(directories[currentDirectory])/\(file.name)")
let filePath = fileURL.path
if manager.fileExists(atPath: filePath) {
if let attributes = try? manager.attributesOfItem(atPath: filePath) {
let type = attributes[.type] as! FileAttributeType
let size = attributes[.size] as! Int
let date = attributes[.creationDate] as! Date
if type != FileAttributeType.typeDirectory {
values.0 = file.name
values.1 = fileURL.pathExtension
values.2 = String(size)
values.3 = date.formatted(date: .abbreviated, time: .omitted)
}
}
}
}
return values
}
이미지 저장하기
시스템은 데이터를 저장할때 0과 1로 된 데이터로 저장을 하므로 이를 위해서 데이터를 저장할 때 시스템이 저장할 수 있는 형태로 변환시켜야합니다. 예를 들어 이미지같은 경우에는 pngData() 메서드를 사용하여 이미지를 pngData로 변환해야합니다. pngData()는 이미지를 PNG 포맷 방식의 raw data로 변환하여 Data 구조로 반환시킵니다.
func saveFile(namePicture: String) {
let image = UIImage(named: namePicture)
if let imageData = image?.pngData() {
let fileURL = docURL.appendingPathComponent("imagedata.dat")
let filePath = fileURL.path
if manager.createFile(atPath: filePath, contents: imageData, attributes: nil) {
imageInFile = image
}
}
}
이미지는 그 외에도 jpegData(compressionQuality: CGFloat) 메서드를 사용하여 jpeg 포맷 방식의 raw data로 변환하여 Data 구조를 반환받을수도 있습니다.
func saveFile(namePicture: String) {
let image = UIImage(named: namePicture)
if let imageData = image?.jpegData(compressionQuality: 0.1) {
let fileURL = docURL.appendingPathComponent("imagedata.dat")
let filePath = fileURL.path
if manager.createFile(atPath: filePath, contents: imageData, attributes: nil) {
imageInFile = image
}
}
}
이미지의 크기를 줄여서 사용하기 위해 다음과 같은 메서드들을 사용할 수 있습니다.
preparingThumbnail(of: CGSize): 원본 이미지에서 인자값만큼의 사이즈로 줄여서 이미지를 반환합니다. 추가로 prepareThumbnail(of: CGSize, completionHandler: Closure): 클로저는 UIImage 객체를 결과로 받습니다.
func saveFile(namePicture: String) {
guard let image = UIImage(named: namePicture) else {
return
}
guard let thumbnail = image.preparingThumbnail(of: CGSize(width: 100, height: 100)) else {
return
}
if let imageData = thumbnail.pngData() {
let fileURL = docURL.appendingPathComponent("imagedata.dat")
let filePath = fileURL.path
if manager.createFile(atPath: filePath, contents: imageData, attributes: nil) {
imageInFile = thumbnail
}
}
}
중요한 것은 UIImage 클래스는 비동기적으로 이미지를 준비할 수 있는 메서드를 제공합니다. bePreparingForDisplay()와 bePreparingThumbnail(ofSize: CGSize)입니다. 비동기적 메서드들을 사용함으로써 이미지를 불러오는 동시에 다른 작업을 수행할 수 있는 장점을 얻을 수 있습니다.
추가로 원본 이미지를 압축을 풀어 화면에 보여줄 이미지를 반환하는 메서드 preparingForDisply() 와 클로저는 UIImage 객체를 결과로 받는 prepareForDisplay(completionHanlder: Closure) 가 있습니다.
텍스트 저장하기
String 타입인 변수 값을 data 타입으로 만들어 contents에 넣어 저장할 수 있게 만들었습니다. 변수는 Published 로 선언하여 값이 매번 바뀔 때마다 값을 파일에 저장하게 됩니다.
@Published var textInFile: String = "" {
didSet {
if let textData = textInFile.data(using: .utf8, allowLossyConversion: true) {
let fileURL = docURL.appendingPathComponent("textdata.dat")
let filePath = fileURL.path
manager.createFile(atPath: filePath, contents: textData, attributes: nil)
}
}
}
아래 코드는 저장을 위한 전체 코드입니다.
class ApplicationData: ObservableObject {
@Published var textInFile: String = "" {
didSet {
if let textData = textInFile.data(using: .utf8, allowLossyConversion: true) {
let fileURL = docURL.appendingPathComponent("textdata.dat")
let filePath = fileURL.path
manager.createFile(atPath: filePath, contents: textData, attributes: nil)
}
}
}
var manager: FileManager
var docURL: URL
init() {
manager = FileManager.default
docURL = manager.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = docURL.appendingPathComponent("textdata.dat")
let filePath = fileURL.path
if manager.fileExists(atPath: filePath) {
if let content = manager.contents(atPath: filePath) {
if let text = String(data: content, encoding: .utf8) {
textInFile = text
}
}
}
}
}
bundle를 사용하기
bundle.path(forResource:_, ofType:_)를 사용하여 bundle에 접근할 수 있습니다.
import SwiftUI
class ApplicationData: ObservableObject {
@Published var textInFile: String = ""
init() {
let manager = FileManager.default
let bundle = Bundle.main
if let path = bundle.path(forResource: "quote", ofType: "txt") {
if let data = manager.contents(atPath: path) {
if let message = String(data: data, encoding: .utf8) {
textInFile = message
}
}
} else {
textInFile = "File Not Found"
}
}
}
Document
Document는 파일에 특정 타입의 정보를 저장하고 가공할 수 있는 컨테이너입니다. Document는 기기에 저장될수도 있고 서버나 iCloud에 저장하여 다른 기기들과 공유할수도 있습니다. SwiftUI가 기능을 포함하고 있기에 앱으로 document를 만들고 수정 및 공유하는 것은 매우 중요하게 여겨집니다.
FileDocument 프로토콜을 준수하며 plainText 타입을 읽고 쓸 수 있는 TextDocument 구조체를 만들고 init(configuration:)을 통해서 기존에 있던 문자열 데이터를 documentText에 넣으며 fileWrapper(configuration: WriteConfiguration)을 사용하여 문서를 파일에 저장할 수 있게 합니다.
struct TextDocument: FileDocument {
static var readableContentTypes: [UTType] = [.plainText]
var documentText: String
init() {
documentText = ""
}
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
if let text = String(data: data, encoding: .utf8) {
documentText = text
} else {
throw CocoaError(.fileReadCorruptFile)
}
} else {
throw CocoaError(.fileReadCorruptFile)
}
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = documentText.data(using: .utf8)
let wrapper = FileWrapper(regularFileWithContents: data!)
return wrapper
}
}
다음으로 Text를 Export하고 Import 할 수 있는 UI를 만들어서 TextEditor에 저장된 값을 Export, Import할 수 있습니다.
import SwiftUI
struct File: Identifiable {
let id: UUID = UUID()
var name: String
}
struct FileContent {
var name: String
var content: String
}
class ApplicationData: ObservableObject {
@Published var listOfFiles: [File] = []
@Published var selectedFile: FileContent
@Published var openExporter: Bool = false
var manager: FileManager
var docURL: URL
var document: TextDocument
init() {
manager = FileManager.default
let documents = manager.urls(for: .documentDirectory, in: .userDomainMask)
docURL = documents.first!
document = TextDocument()
selectedFile = FileContent(name: "", content: "")
if let list = try? manager.contentsOfDirectory(atPath: docURL.path) {
for name in list {
let newFile = File(name: name)
listOfFiles.append(newFile)
}
}
}
func saveFile(name: String) {
let newFileURL = docURL.appendingPathComponent(name)
let path = newFileURL.path
manager.createFile(atPath: path, contents: nil, attributes: nil)
if !listOfFiles.contains(where: { $0.name == name}) {
listOfFiles.append(File(name: name))
}
}
func saveContent() {
if let data = selectedFile.content.data(using: .utf8, allowLossyConversion: true) {
let path = docURL.appendingPathComponent(selectedFile.name).path
manager.createFile(atPath: path, contents: data, attributes: nil)
}
}
func exportDocument(file: File) {
let content = getDocumentContent(file: file)
selectedFile = FileContent(name: file.name, content: content)
document.documentText = content
openExporter = true
}
func getDocumentContent(file: File) -> String {
selectedFile.name = file.name
let path = docURL.appendingPathComponent(file.name).path
if manager.fileExists(atPath: path) {
if let data = manager.contents(atPath: path) {
if let content = String(data: data, encoding: .utf8) {
return content
}
}
}
return ""
}
}
import SwiftUI
struct ContentView: View {
@EnvironmentObject var appData: ApplicationData
@State private var openSheet: Bool = false
var body: some View {
NavigationStack {
VStack {
List {
ForEach(appData.listOfFiles) { file in
NavigationLink(destination: {
EditFileView(file: file)
} , label: {
HStack {
Text(file.name)
Spacer()
Button(action: {
appData.exportDocument(file: file)
}, label: {
Image(systemName: "square.and.arrow.up")
}).buttonStyle(.plain)
}
})
}
}.listStyle(.plain)
}.padding()
.navigationBarTitle("Files")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add File") {
openSheet = true
}
}
}
.sheet(isPresented: $openSheet) {
AddFileView()
}
}
}
}
'SwiftUI' 카테고리의 다른 글
Bundle (0) | 2023.11.12 |
---|---|
FileDocument (0) | 2023.11.12 |
Button (0) | 2023.11.11 |
wrappedValue, projectedValue (0) | 2023.11.10 |
property wrapper (0) | 2023.11.10 |