MongoDB
 sql >> Cơ Sở Dữ Liệu >  >> NoSQL >> MongoDB

Cách tôi đã viết một ứng dụng đứng đầu bảng xếp hạng trong một tuần với Realm và SwiftUI

Xây dựng công cụ theo dõi nhiệm vụ nhẫn Elden

Tôi yêu Skyrim. Tôi vui vẻ dành hàng trăm giờ để chơi và chơi lại nó. Vì vậy, gần đây tôi nghe nói về một trò chơi mới, Skyrim của những năm 2020 , Tôi đã phải mua nó. Vì vậy, bắt đầu câu chuyện của tôi với Elden Ring, game nhập vai thế giới mở lớn với sự hướng dẫn câu chuyện từ George R.R. Martin.

Trong vòng một giờ đầu tiên của trò chơi, tôi đã biết được rằng trò chơi Souls có thể tàn bạo như thế nào. Tôi len lỏi vào những hang động thú vị bên vách đá chỉ để chết bên trong đến mức không thể lấy được xác của mình.

Tôi đã mất tất cả các rune của mình.

Tôi há hốc mồm trong sự kinh ngạc chờ đợi khi đi thang máy xuống sông Siofra, chỉ để thấy rằng cái chết rùng rợn đang chờ đợi tôi, cách xa địa điểm ân sủng gần nhất. Tôi dũng cảm bỏ chạy trước khi tôi có thể chết một lần nữa.

Tôi đã gặp những nhân vật ma quái và những NPC hấp dẫn, những người đã cám dỗ tôi bằng một vài câu thoại… mà tôi ngay lập tức quên ngay khi cần.

10/10, rất khuyến khích.

Một điều đặc biệt về Elden Ring khiến tôi khó chịu - không có công cụ theo dõi nhiệm vụ. Tôi đã mở một tài liệu Ghi chú trên iPhone của mình. Tất nhiên, điều đó gần như là chưa đủ.

Tôi cần một ứng dụng để giúp tôi theo dõi các chi tiết chơi RPG. Không có gì trên App Store thực sự phù hợp với những gì tôi đang tìm kiếm, vì vậy rõ ràng tôi sẽ cần phải viết nó. Nó có tên là Shattered Ring và hiện có trên App Store.

Lựa chọn công nghệ

Hàng ngày, tôi viết tài liệu cho Realm Swift SDK. Gần đây, tôi đã viết một ứng dụng mẫu SwiftUI cho Realm để cung cấp cho các nhà phát triển mẫu khởi động SwiftUI để xây dựng, hoàn chỉnh với các quy trình đăng nhập. Nhóm Realm Swift SDK đã liên tục vận chuyển các tính năng SwiftUI, điều này đã khiến nó - theo quan điểm có lẽ là thiên vị của tôi - một điểm khởi đầu đơn giản chết người để phát triển ứng dụng.

Tôi muốn thứ gì đó mà tôi có thể xây dựng siêu nhanh - một phần để tôi có thể quay lại chơi Elden Ring thay vì viết ứng dụng và một phần để đánh bại các ứng dụng khác trên thị trường trong khi mọi người vẫn đang nói về Elden Ring. Tôi không thể mất nhiều tháng để tạo ứng dụng này. Tôi đã muốn nó ngày hôm qua. Realm + SwiftUI sẽ biến điều đó thành khả thi.

Mô hình hóa dữ liệu

Tôi biết rằng tôi muốn theo dõi các nhiệm vụ trong trò chơi. Mô hình nhiệm vụ rất dễ dàng:

class Quest: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var isComplete = false
    @Persisted var notes = ""
}

Tất cả những gì tôi thực sự cần là một cái tên, một công cụ để chuyển đổi khi nhiệm vụ hoàn thành, một trường ghi chú và một số nhận dạng duy nhất.

Tuy nhiên, khi nghĩ về cách chơi của mình, tôi nhận ra rằng tôi không chỉ cần nhiệm vụ - tôi còn muốn theo dõi các vị trí. Tôi tình cờ đến - và nhanh chóng thoát khỏi khi tôi bắt đầu chết - rất nhiều nơi thú vị có thể có các nhân vật không phải người chơi (NPC) thú vị và chiến lợi phẩm tuyệt vời. Tôi muốn có thể theo dõi xem tôi đã xóa một vị trí hay chỉ chạy khỏi vị trí đó, vì vậy tôi có thể nhớ quay lại sau và kiểm tra nó khi tôi có thiết bị tốt hơn và nhiều khả năng hơn. Vì vậy, tôi đã thêm một đối tượng vị trí:

class Location: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var isCleared = false
    @Persisted var notes = ""
}

Hừ! Điều đó trông rất giống với mô hình nhiệm vụ. Tôi đã thực sự cần một đối tượng riêng biệt? Sau đó, tôi nghĩ về một trong những địa điểm đầu tiên tôi đến thăm - Nhà thờ Elleh - nơi có một cái đe thợ rèn. Tôi chưa thực sự làm bất cứ điều gì để cải thiện thiết bị của mình, nhưng có thể rất tuyệt nếu biết những địa điểm nào có đe thợ rèn trong tương lai khi tôi muốn đến một nơi nào đó để nâng cấp. Vì vậy, tôi đã thêm một bool khác:

@Persisted var hasSmithAnvil = false

Sau đó, tôi nghĩ về cách mà cùng một địa điểm cũng có một thương gia. Tôi có thể muốn biết liệu một vị trí có người bán trong tương lai hay không. Vì vậy, tôi đã thêm một bool khác:

@Persisted var hasMerchant = false

Tuyệt quá! Đối tượng vị trí đã được sắp xếp.

Nhưng có một cái gì đó khác. Tôi tiếp tục nhận được tất cả những mẩu chuyện thú vị này từ các NPC. Và điều gì đã xảy ra khi tôi hoàn thành một nhiệm vụ - tôi có cần quay lại gặp NPC để nhận phần thưởng không? Điều đó sẽ yêu cầu tôi biết ai đã giao cho tôi nhiệm vụ và họ đang ở đâu. Đã đến lúc thêm một mô hình thứ ba, NPC, sẽ gắn kết mọi thứ lại với nhau:

NPC
class NPC: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var isMerchant = false
    @Persisted var locations = List<Location>()
    @Persisted var quests = List<Quest>()
    @Persisted var notes = ""
}

Tuyệt quá! Bây giờ tôi có thể theo dõi các NPC. Tôi có thể thêm ghi chú để giúp tôi theo dõi những mẩu chuyện thú vị đó trong khi chờ xem điều gì sẽ diễn ra. Tôi có thể liên kết các nhiệm vụ và địa điểm với NPC. Sau khi thêm đối tượng này, rõ ràng đây là đối tượng kết nối những đối tượng khác. NPC có mặt tại các địa điểm. Nhưng tôi biết từ một số đọc trực tuyến rằng đôi khi các NPC di chuyển xung quanh trong trò chơi, vì vậy các vị trí sẽ phải hỗ trợ nhiều mục nhập - do đó mới có danh sách. NPC đưa ra các nhiệm vụ. Nhưng đó cũng nên là một danh sách, bởi vì NPC đầu tiên tôi gặp đã cho tôi nhiều hơn một nhiệm vụ. Varre, ngay bên ngoài Nghĩa địa bị vỡ khi bạn lần đầu tiên tham gia trò chơi, đã bảo tôi “Hãy làm theo những sợi dây của ân sủng” và “hãy đến lâu đài”. Đúng, đã sắp xếp!

Bây giờ tôi có thể sử dụng các đối tượng của mình với các trình bao bọc thuộc tính SwiftUI để bắt đầu tạo giao diện người dùng.

SwiftUI Views + Realm’s Magical Property Wrappers

Vì mọi thứ đều treo ngược với NPC, tôi sẽ bắt đầu với các lượt xem NPC. @ObservedResults trình bao bọc thuộc tính cung cấp cho bạn một cách dễ dàng để thực hiện việc này.

struct NPCListView: View {
    @ObservedResults(NPC.self) var npcs

    var body: some View {
        VStack {
            List {
                ForEach(npcs) { npc in
                    NavigationLink {
                        NPCDetailView(npc: npc)
                    } label: {
                        NPCRow(npc: npc)
                    }
                }
                .onDelete(perform: $npcs.remove)
                .navigationTitle("NPCs")
            }
            .listStyle(.inset)
        }
    }
}

Bây giờ tôi có thể lặp lại danh sách tất cả các NPC, có một onDelete tự động hành động để xóa NPC và có thể thêm việc triển khai .searchable của Realm khi tôi đã sẵn sàng thêm tìm kiếm và lọc. Và về cơ bản nó là một dòng để kết nối nó với mô hình dữ liệu của tôi. Tôi đã đề cập đến Realm + SwiftUI có tuyệt vời không? Thật dễ dàng để thực hiện điều tương tự với Vị trí và Nhiệm vụ, đồng thời giúp người dùng ứng dụng có thể đi sâu vào dữ liệu của họ thông qua bất kỳ đường dẫn nào.

Sau đó, chế độ xem chi tiết NPC của tôi có thể hoạt động với @ObservedRealmObject trình bao bọc thuộc tính để hiển thị chi tiết NPC và giúp dễ dàng chỉnh sửa NPC:

struct NPCDetailView: View {
    @ObservedRealmObject var npc: NPC

    var body: some View {
        VStack {
            HStack {
            Text("Notes")
                 .font(.title2)
                 Spacer()
            if npc.isMerchant {
                Image(systemName: "dollarsign.square.fill")
            }
        Spacer()
        Text($npc.notes)
        Spacer()
        }
    }
}

Một lợi ích khác của @ObservedRealmObject là tôi có thể sử dụng $ ký hiệu để bắt đầu viết nhanh, vì vậy trường ghi chú sẽ có thể chỉnh sửa được. Người dùng có thể nhấn vào và chỉ cần thêm các ghi chú khác và Realm sẽ chỉ lưu các thay đổi. Không cần chế độ xem chỉnh sửa riêng biệt hoặc mở một giao dịch viết rõ ràng để cập nhật ghi chú.

Tại thời điểm này, tôi đã có một ứng dụng đang hoạt động và tôi có thể dễ dàng vận chuyển nó.

Nhưng… tôi đã có một suy nghĩ.

Một trong những điều tôi yêu thích ở các game RPG thế giới mở là chơi lại chúng dưới dạng các nhân vật khác nhau và với các lựa chọn khác nhau. Vì vậy, có lẽ tôi muốn chơi lại Elden Ring như một lớp khác. Hoặc - có thể đây không phải là một trình theo dõi Elden Ring cụ thể, nhưng có lẽ tôi có thể sử dụng nó để theo dõi bất kỳ trò chơi RPG nào. Còn các trò chơi D&D của tôi thì sao?

Nếu tôi muốn theo dõi nhiều trò chơi, tôi cần thêm thứ gì đó vào mô hình của mình. Tôi cần một khái niệm về một thứ gì đó như một trò chơi hoặc một trò chơi.

Lặp lại trên Mô hình Dữ liệu

Tôi cần một số đối tượng để bao gồm các NPC, Vị trí và Nhiệm vụ là một phần của cái này playthrough, vì vậy tôi có thể giữ chúng tách biệt với các playthrough khác. Vậy nếu đó là một Trò chơi thì sao?

class Game: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name = ""
    @Persisted var npcs = List<NPC>()
    @Persisted var locations = List<Location>()
    @Persisted var quests = List<Quest>()
}

Ổn thỏa! Tuyệt quá. Giờ đây, tôi có thể theo dõi các NPC, Vị trí và Nhiệm vụ trong trò chơi này và giữ chúng khác biệt với các trò chơi khác.

Đối tượng Trò chơi rất dễ hình dung, nhưng khi tôi bắt đầu nghĩ về @ObservedResults theo quan điểm của tôi, tôi nhận ra rằng điều đó sẽ không còn hiệu quả nữa. @ObservedResults trả về tất cả các kết quả cho một loại đối tượng cụ thể. Vì vậy, nếu tôi muốn chỉ hiển thị các NPC cho trò chơi này, tôi cần phải thay đổi quan điểm của mình. *

  • Swift SDK phiên bản 10.24.0 đã thêm khả năng sử dụng cú pháp Truy vấn Swift trong @ObservedResults , cho phép bạn lọc kết quả bằng where tham số. Tôi chắc chắn đang cấu trúc lại để sử dụng cái này trong một phiên bản trong tương lai! Nhóm SDK Swift đã liên tục phát hành các tính năng mới của SwiftUI.

Ồ. Ngoài ra, tôi cần một cách để phân biệt các NPC trong trò chơi này với các NPC trong các trò chơi khác. Hrm. Bây giờ có thể là lúc để xem xét liên kết ngược. Sau khi quay vòng trong Tài liệu SDK Realm Swift, tôi đã thêm cái này vào mô hình NPC:

@Persisted(originProperty: "npcs") var npcInGame: LinkingObjects<Game>

Bây giờ tôi có thể liên kết ngược các NPC với đối tượng Trò chơi. Nhưng, than ôi, bây giờ quan điểm của tôi trở nên phức tạp hơn.

Cập nhật Chế độ xem SwiftUI cho các Thay đổi Mô hình

Vì bây giờ tôi chỉ muốn một tập hợp con các đối tượng của mình (và đây là trước @ObservedResults cập nhật), tôi đã chuyển chế độ xem danh sách của mình từ @ObservedResults tới @ObservedRealmObject , quan sát trò chơi:

@ObservedRealmObject var game: Game

Bây giờ tôi vẫn nhận được lợi ích của việc viết nhanh để thêm và chỉnh sửa NPC, Vị trí và Nhiệm vụ trong trò chơi, nhưng mã Danh sách của tôi phải cập nhật một chút:

ForEach(game.npcs) { npc in
    NavigationLink {
        NPCDetailView(npc: npc)
    } label: {
        NPCRow(npc: npc)
    }
}
.onDelete(perform: $game.npcs.remove

Vẫn không phải là xấu, nhưng một mức độ khác của các mối quan hệ để xem xét. Và vì điều này không sử dụng @ObservedResults , Tôi không thể sử dụng triển khai Vương quốc của .searchable , nhưng sẽ phải tự mình thực hiện. Không phải là một vấn đề lớn, nhưng nhiều công việc hơn.

Đối tượng đông lạnh và thêm vào danh sách

Bây giờ, cho đến thời điểm này, tôi có một ứng dụng đang hoạt động. Tôi có thể gửi cái này như hiện tại. Mọi thứ vẫn đơn giản với trình bao bọc thuộc tính Realm Swift SDK thực hiện tất cả công việc.

Nhưng tôi muốn ứng dụng của mình làm được nhiều hơn thế.

Tôi muốn có thể thêm Địa điểm và Nhiệm vụ từ chế độ xem NPC và tự động thêm chúng vào NPC. Và tôi muốn có thể xem và thêm người cho nhiệm vụ từ chế độ xem nhiệm vụ. Và tôi muốn có thể xem và thêm NPC vào các vị trí từ chế độ xem vị trí.

Tất cả những điều này đều yêu cầu thêm rất nhiều vào danh sách và khi tôi bắt đầu cố gắng thực hiện việc này bằng cách viết nhanh sau khi tạo đối tượng, tôi nhận ra rằng điều đó sẽ không hiệu quả. Tôi phải chuyển các đối tượng xung quanh và nối chúng theo cách thủ công.

Những gì tôi muốn là làm một cái gì đó như thế này:

func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
    let realm = try! Realm()
    let thisLocation = game.locations.where { $0.name == locationName }.first!

    try! realm.write {
        npc!.locations.append(thisLocation)
    }
}

Đây là nơi mà điều gì đó không hoàn toàn rõ ràng đối với tôi khi một nhà phát triển mới bắt đầu cản đường tôi. Tôi chưa bao giờ thực sự phải làm bất cứ điều gì với các đối tượng phân luồng và đóng băng trước đây, nhưng tôi đã gặp sự cố mà thông báo lỗi khiến tôi nghĩ rằng điều này có liên quan đến điều đó. May mắn thay, tôi nhớ đã viết một ví dụ mã về việc làm tan băng các đối tượng bị đóng băng để bạn có thể làm việc với chúng trên các chuỗi khác, vì vậy, nó đã quay trở lại tài liệu - lần này là trang Phân luồng bao gồm các Đối tượng Đông lạnh. (Các cải tiến khác mà nhóm Realm Swift SDK đã thêm vào kể từ khi tôi tham gia MongoDB - yay!)

Sau khi truy cập các tài liệu, tôi có một cái gì đó như thế này:

func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
    let realm = try! Realm()
    Let thawedNPC = npc.thaw()
    let thisLocation = game.locations.where { $0.name == locationName }.first!

    try! realm.write {
        thawedNPC!.locations.append(thisLocation)
    }
}

Điều đó có vẻ đúng, nhưng vẫn bị lỗi. Nhưng tại sao? (Đây là lúc tôi tự nguyền rủa bản thân vì đã không cung cấp một ví dụ mã kỹ lưỡng hơn trong tài liệu. Làm việc trên ứng dụng này chắc chắn đã tạo ra một số vé để cải thiện tài liệu của chúng tôi trong một số lĩnh vực!)

Sau khi quay vòng trên các diễn đàn và tham khảo ý kiến ​​của người tiên tri tuyệt vời của Google, tôi tình cờ gặp một chủ đề nơi ai đó đang nói về vấn đề này. Hóa ra, bạn phải làm tan băng không chỉ vật bạn đang cố gắn vào mà còn phải làm tan vật bạn đang cố nối vào. Điều này có thể rõ ràng với một nhà phát triển có kinh nghiệm hơn, nhưng nó đã khiến tôi vấp phải một thời gian. Vì vậy, những gì tôi thực sự cần là một cái gì đó như thế này:

func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
    let realm = try! Realm()
    let thawedNpc = npc.thaw()
    let thisLocation = game.locations.where { $0.name == locationName     }.first!
    let thawedLocation = thisLocation.thaw()!

    try! realm.write {
        thawedNpc!.locations.append(thawedLocation)
    }
}

Tuyệt quá! Vấn đề đã được giải quyết. Giờ đây, tôi có thể tạo tất cả các funcs mà tôi cần để xử lý thủ công việc nối thêm (và loại bỏ, khi nó xảy ra) của các đối tượng.

Mọi thứ khác chỉ là SwiftUI

Sau đó, mọi thứ khác mà tôi phải học để tạo ra ứng dụng chỉ là SwiftUI, như cách lọc, cách làm cho bộ lọc có thể người dùng chọn và cách triển khai phiên bản .searchable của riêng tôi .

Chắc chắn có một số điều tôi đang làm với điều hướng kém hơn mức tối ưu. Tôi vẫn muốn thực hiện một số cải tiến về trải nghiệm người dùng. Và chuyển đổi trò chơi @ObservedRealmObject var game: Game của tôi quay lại @ObservedResults với công cụ lọc mới sẽ giúp thực hiện một số cải tiến đó. Nhưng nhìn chung, trình bao bọc thuộc tính Realm Swift SDK đã làm cho việc triển khai ứng dụng này trở nên đơn giản đến mức ngay cả tôi cũng có thể làm được.

Tổng cộng, tôi đã xây dựng ứng dụng trong hai ngày cuối tuần và một số ít các đêm trong tuần. Có lẽ một ngày cuối tuần của thời gian đó là tôi bị mắc kẹt với vấn đề liên quan đến danh sách, đồng thời tạo trang web cho ứng dụng, nhận tất cả ảnh chụp màn hình để gửi lên App Store và tất cả những thứ “kinh doanh” đi kèm với việc trở thành một nhà phát triển ứng dụng độc lập.

Nhưng tôi ở đây để nói với bạn rằng nếu tôi, một nhà phát triển ít kinh nghiệm với chính xác một ứng dụng trước đó mang tên tôi - và điều đó với rất nhiều phản hồi từ trưởng nhóm của tôi - cũng có thể tạo ra một ứng dụng như Shattered Ring. Và thật dễ dàng hơn rất nhiều với SwiftUI + các tính năng SwiftUI của Realm Swift SDK. Hãy xem phần Bắt đầu nhanh SwiftUI để biết ví dụ điển hình để xem nó dễ dàng như thế nào.


  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Cuộc chiến của các cơ sở dữ liệu NoSQL - So sánh MongoDB và Firebase

  2. Cách cài đặt MongoDB trên Ubuntu 18.04

  3. Lỗi truyền JSON.NET khi tuần tự hóa Mongo ObjectId

  4. MongoDB $ round so với $ trunc:Sự khác biệt là gì?

  5. Mongo sắp xếp theo một điều kiện được tính toán