聊一下在SwiftUI中使用CoreData

本文并非一個(gè)教你如何在SwiftUI下使用CoreData的教程肴沫。主要探討的是在我近一年的SwiftUI開(kāi)發(fā)中使用CoreData的教訓(xùn)略贮、經(jīng)驗(yàn)、心得茫陆。

SwiftUI lifecycle 中如何聲明持久化存儲(chǔ)和上下文

在XCode12中金麸,蘋(píng)果新增了SwiftUI lifecycle,讓App完全的SwiftUI化簿盅。不過(guò)這就需要我們使用新的方法來(lái)聲明持久化存儲(chǔ)和上下文挥下。

好像是從beta6開(kāi)始,XCode 12提供了基于SwiftUI lifecycle的CoreData模板

@main
struct CoreDataTestApp: App {
    //持久化聲明
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)  
          //上下文注入
        }
    }
}

在它的Presitence中桨醋,添加了用于preview的持久化定義

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        //根據(jù)你的實(shí)際需要棚瘟,創(chuàng)建用于preview的數(shù)據(jù)
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentCloudKitContainer
    //如果是用于preview便將數(shù)據(jù)保存在內(nèi)存而非sqlite中
    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "Shared")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

雖然對(duì)于用于preview的持久化設(shè)置并不完美,不過(guò)蘋(píng)果也意識(shí)到了在SwiftUI1.0中的一個(gè)很大問(wèn)題讨盒,無(wú)法preview使用了@FetchRequest的視圖解取。

由于在官方CoreData模板出現(xiàn)前,我已經(jīng)開(kāi)始了我的項(xiàng)目構(gòu)建返顺,因此禀苦,我使用了下面的方式來(lái)聲明

struct HealthNotesApp:App{
  static let coreDataStack = CoreDataStack(modelName: "Model") //Model.xcdatemodeld
  static let context = DataNoteApp.coreDataStack.managedContext
  static var storeRoot = Store() 
   @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
  WindowGroup {
        rootView()
            .environmentObject(store)
                    .environment(\.managedObjectContext, DataNoteApp.context)
  }
}

在UIKit App Delegate中,我們可以使用如下代碼在App任意位置獲取上下文

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

但由于我們已經(jīng)沒(méi)有辦法在SwiftUI lifecycle中如此使用遂鹊,通過(guò)上面的聲明我們可以利用下面的方法在全局獲取想要的上下文或其他想要獲得的對(duì)象

let context = HealthNotesApp.context

比如在 delegate中

class AppDelegate:NSObject,UIApplicationDelegate{
    
    let send = HealthNotesApp.storeRoot.send
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       
        logDebug("app startup on ios")
       
        send(.loadNote)
        return true
    }

    func applicationDidFinishLaunching(_ application: UIApplication){
        
        logDebug("app quit on ios")
        send(.counter(.save))

    }

}

//或者直接操作數(shù)據(jù)庫(kù)振乏,都是可以的

如何動(dòng)態(tài)設(shè)置 @FetchRequest

在SwiftUI中,如果無(wú)需復(fù)雜的數(shù)據(jù)操作秉扑,使用CoreData是非常方便的慧邮。在完成xcdatamodeld的設(shè)置后,我們就可以在View中輕松的操作數(shù)據(jù)了舟陆。

我們通常使用如下語(yǔ)句來(lái)獲取某個(gè)entity的數(shù)據(jù)

@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.studentId, ascending: true)],
              predicate:NSPredicate(format: "age > 10"),
              animation: .default) 
private var students: FetchedResults<Student>

不過(guò)如此使用的話误澳,查詢(xún)條件將無(wú)法改變,如果想根據(jù)需要調(diào)整查詢(xún)條件秦躯,可以使用下面的方法忆谓。

健康筆記2中的部分代碼:

struct rootView:View{
        @State var predicate:NSPredicate? = nil
    @State var sort = NSSortDescriptor(key: "date", ascending: false)
    @StateObject var searchStore = SearchStore()
    @EnvironmentObject var store:Store
    var body:some View{
      VStack {
       SearchBar(text: $searchStore.searchText) //搜索框
       MemoList(predicate: predicate, sort: sort,searching:searchStore.showSearch)
        }
      .onChange(of: searchStore.text){ _ in
          getMemos()
      }
    }
  
       //讀取指定范圍的memo
    func getMemos() {
        var predicators:[NSPredicate] = []
        if !searchStore.searchText.isEmpty && searchStore.showSearch {
            //memo內(nèi)容或者item名稱(chēng)包含關(guān)鍵字
            predicators.append(NSPredicate(format: "itemData.item.name contains[cd] %@ OR content contains[cd] %@", searchStore.searchText,searchStore.searchText))
        }
        if star {
            predicators.append(NSPredicate(format: "star = true"))
        }
        
        switch store.state.memo{
        case .all:
            break
        case .memo:
            if !searchStore.searchText.isEmpty && noteOption == 1 {
                break
            }
            else {
                predicators.append(NSPredicate(format: "itemData.item.note = nil"))
            }
        case .note(let note):
            if !searchStore.searchText.isEmpty && noteOption == 1 {
                break
            }
            else {
                predicators.append(NSPredicate(format: "itemData.item.note = %@", note))
            }
        }
        
        withAnimation(.easeInOut){
            predicate =  NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.and, subpredicates: predicators)
            sort =  NSSortDescriptor(key: "date", ascending: ascending)
        }
    }
}

上述代碼會(huì)根據(jù)搜索關(guān)鍵字以及一些其他的范圍條件,動(dòng)態(tài)的創(chuàng)建predicate踱承,從而獲得所需的數(shù)據(jù)倡缠。

對(duì)于類(lèi)似查詢(xún)這樣的操作,最好配合上Combine來(lái)限制數(shù)據(jù)獲取的頻次

例如:

class SearchStore:ObservableObject{
    @Published var searchText = ""
    @Published var text = ""
    @Published var showSearch = false
    
    private var cancellables:[AnyCancellable] = []
    
    func registerPublisher(){
        $searchText
            .removeDuplicates()
            .debounce(for: 0.4, scheduler: DispatchQueue.main)
            .assign(to: &$text)
    }
    
    func removePublisher(){
        cancellables.removeAll()
    }
    
}

上述所有代碼均缺失了很大部分茎活,僅做思路上的說(shuō)明

增加轉(zhuǎn)換層方便代碼開(kāi)發(fā)

在開(kāi)發(fā)健康筆記 1.0的時(shí)候我經(jīng)常被類(lèi)似下面的代碼所煩惱

@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.name, ascending: true)],
              animation: .default) 
private var students: FetchedResults<Student>

ForEach(students){ student in
  Text(student.name ?? "")
  Text(String(student.date ?? Date()))
}

在CoreData中昙沦,設(shè)置Attribute,很多時(shí)候并不能完全如愿载荔。

好幾個(gè)類(lèi)型是可選的盾饮,比如String,UUID等,如果在已發(fā)布的app丐谋,將新增的attribute其改為不可選芍碧,并設(shè)置默認(rèn)值煌珊,將極大的增加遷移的難度号俐。另外,如果使用了NSPersistentCloudKitContainer,由于Cloudkit的atrribute和CoreData并不相同定庵,XCode會(huì)強(qiáng)制你將很多Attribute改成你不希望的樣式吏饿。

為了提高開(kāi)發(fā)效率,并為未來(lái)的修改留出靈活蔬浙、充分的更改空間猪落,在健康筆記2.0的開(kāi)發(fā)中,我為每個(gè)NSManagedObject都增加了一個(gè)便于在View和其他數(shù)據(jù)操作中使用的中間層畴博。

例如:

@objc(Student)
public class Student: NSManagedObject,Identifiable {
    @NSManaged public var name: String?
    @NSmanaged public var birthdate: Date?
}

public struct StudentViewModel: Identifiable{
    let name:String
    let birthdate:String
}

extension Student{
   var viewModel:StudentViewModel(
        name:name ?? ""
        birthdate:(birthdate ?? Date()).toString() //舉例
   )
  
}

如此一來(lái)笨忌,在View中調(diào)用將非常方便,同時(shí)即使更改entity的設(shè)置俱病,整個(gè)程序的代碼修改量也將顯著降低官疲。

ForEach(students){ student in
  let student = student.viewModel
  Text(student.name)
  Text(student.birthdate)
}

同時(shí),對(duì)于數(shù)據(jù)的其他操作亮隙,我也都通過(guò)這個(gè)viewModel來(lái)完成途凫。

比如:


//MARK:通過(guò)ViewModel生成Note數(shù)據(jù),所有的prepare動(dòng)作都需要顯示調(diào)用 _coreDataSave()
    func _prepareNote(_ viewModel:NoteViewModel) -> Note{
        let note = Note(context: context )
        note.id = viewModel.id 
        note.index = Int32(viewModel.index)  
        note.createDate = viewModel.createDate  
        note.name = viewModel.name 
        note.source = Int32(viewModel.source)  
        note.descriptionContent = viewModel.descriptionContent 
        note.color = viewModel.color.rawValue 
        return note
    }
    
    //MARK:更新Note數(shù)據(jù),仍需顯示調(diào)用save
    func _updateNote(_ note:Note,_ viewModel:NoteViewModel) -> Note {
        note.name = viewModel.name
        note.source = Int32(viewModel.source)
        note.descriptionContent = viewModel.descriptionContent
        note.color = viewModel.color.rawValue
        return note
    }

func newNote(noteViewModel:NoteViewModel) -> AnyPublisher<AppAction,Never> {
       let _ = _prepareNote(noteViewModel)
       if  !_coreDataSave() {
            logDebug("新建Note出現(xiàn)錯(cuò)誤")
       }
       return Just(AppAction.none).eraseToAnyPublisher()
    }
    
func editNote(note:Note,newNoteViewModel:NoteViewModel) -> AnyPublisher<AppAction,Never>{
        let _ = _updateNote(note, newNoteViewModel)
        if !_coreDataSave() {
            logDebug("更新Note出現(xiàn)錯(cuò)誤")
        }
        return Just(AppAction.none).eraseToAnyPublisher()
}

在View中調(diào)用

Button("New"){
      let noteViewModel = NoteViewModel(createDate: Date(), descriptionContent: myState.noteDescription, id: UUID(), index: -1, name: myState.noteName, source: 0, color: .none)
     store.send(.newNote(noteViewModel: noteViewModel))
     presentationMode.wrappedValue.dismiss()
}

從而將可選值或者類(lèi)型轉(zhuǎn)換控制在最小范圍

使用NSPersistentCloudKitContainer 需要注意的問(wèn)題

從iOS13開(kāi)始,蘋(píng)果提供了NSPersistentCloudKitContainer溢吻,讓app可以以最簡(jiǎn)單的方式享有了數(shù)據(jù)庫(kù)云同步功能维费。

不過(guò)在使用中,我們需要注意幾個(gè)問(wèn)題促王。

  • Attribute
    在上一節(jié)提高過(guò)犀盟,由于Cloudkit的數(shù)據(jù)設(shè)定和CoreData并不完全兼容,因此如果你在項(xiàng)目初始階段是使用NSPersistentContainer進(jìn)行開(kāi)發(fā)的蝇狼,當(dāng)將代碼改成NSPersistentCloudKitContainer后阅畴,XCode可能會(huì)提示你某些Attribute不兼容的情況。如果你采用了中間層處理數(shù)據(jù)题翰,修改起來(lái)會(huì)很方便恶阴,否則你需要對(duì)已完成的代碼做出不少的修改和調(diào)整。我通常為了開(kāi)發(fā)調(diào)試的效率豹障,只有到最后的時(shí)候才會(huì)使用NSPersistentCloudKitContainer冯事,因此這個(gè)問(wèn)題會(huì)比較突出。

  • 合并策略
    奇怪的是血公,在XCode的CoreData(點(diǎn)選使用CloudKit)默認(rèn)模板中昵仅,并沒(méi)有設(shè)定合并策略。如果沒(méi)有設(shè)置的話,當(dāng)app的數(shù)據(jù)進(jìn)行云同步時(shí)摔笤,時(shí)長(zhǎng)會(huì)出現(xiàn)合并錯(cuò)誤够滑,并且@FetchRequest也并不會(huì)在有數(shù)據(jù)發(fā)生變動(dòng)時(shí)對(duì)View進(jìn)行刷新。因此我們需要自己明確數(shù)據(jù)的合并策略吕世。

        lazy var persistentContainer: NSPersistentCloudKitContainer = {
            let container = NSPersistentCloudKitContainer(name: modelName)
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
            //需要顯式表明下面的合并策略,否則會(huì)出現(xiàn)合并錯(cuò)誤!
            container.viewContext.automaticallyMergesChangesFromParent = true
            container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
            return container
        }()
    
  • 調(diào)試信息
    當(dāng)打開(kāi)云同步后彰触,在調(diào)試信息中將出現(xiàn)大量的數(shù)據(jù)同步調(diào)試信息,嚴(yán)重影響了對(duì)于其他調(diào)試信息的觀察命辖。雖然可以通過(guò)啟動(dòng)命令屏蔽掉數(shù)據(jù)同步信息况毅,但有時(shí)候我還是需要對(duì)其進(jìn)行觀察的。目前我使用了一個(gè)臨時(shí)的解決方案尔艇。

    #if !targetEnvironment(macCatalyst) && canImport(OSLog)
    import OSLog
    let logger = Logger.init(subsystem: "com.fatbobman.DataNote", category: "main") //調(diào)試用
    func logDebug(_ text:String,enable:Bool = true){
        #if DEBUG
        if enable {
            logger.debug("\(text)")
        }
        #endif
    }
    #else
    func logDebug(_ text:String,enable:Bool = true){
        print(text,"$$$$")
    }
    #endif
    

    對(duì)于需要顯示調(diào)試信息的地方

    logDebug("數(shù)據(jù)格式錯(cuò)誤")
    

    然后通過(guò)在Debug窗口中將Filter設(shè)置為$$$$來(lái)屏蔽掉暫時(shí)不想看到的其他信息

不要用SQL的思維限制了CoreData的能力

CoreData雖然主要是采用Sqlite來(lái)作為數(shù)據(jù)存儲(chǔ)方案尔许,不過(guò)對(duì)于它的數(shù)據(jù)對(duì)象操作不要完全套用Sql中的慣用思維。

一些例子

排序:

//Sql式的
NSSortDescriptor(key: "name", ascending: true)
//更CoreData化终娃,不會(huì)出現(xiàn)拼寫(xiě)錯(cuò)誤
NSSortDescriptor(keyPath: \Student.name, ascending: true)

在斷言中不適用子查詢(xún)而直接比較對(duì)象:

NSPredicate(format: "itemData.item.name = %@",name)

Count:

func _getCount(entity:String,predicate:NSPredicate?) -> Int{
        let fetchRequest = NSFetchRequest<NSNumber>(entityName: entity)  
        fetchRequest.predicate = predicate
        fetchRequest.resultType = .countResultType
        
        do {
            let results  = try context.fetch(fetchRequest)
            let count = results.first!.intValue
            return count
        }
        catch {
            #if DEBUG
            logDebug("\(error.localizedDescription)")
            #endif
            return 0
        }
    }

或者更加簡(jiǎn)單的count

@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.name, ascending: true)],
              animation: .default) 
private var students: FetchedResults<Student>

sutudents.count

對(duì)于數(shù)據(jù)量不大的情況味廊,我們也可以不采用上面的動(dòng)態(tài)predicate方式,在View中直接對(duì)獲取后的數(shù)據(jù)進(jìn)行操作棠耕,比如:

@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.name, ascending: true)],
              animation: .default) 
private var studentDatas: FetchedResults<Student>
@State var students:[Student] = []
var body:some View{
  List{
        ForEach(students){ student in
           Text(student.viewModel.name)
         }
        }
        .onReceive(studentDatas.publisher){ _ in
            students = studentDatas.filter{ student in
                student.viewModel.age > 10
            }
        }
   }
}

總之?dāng)?shù)據(jù)皆對(duì)象

遺憾和不足

蘋(píng)果在努力提高CoreData在SwiftUI下的表現(xiàn)余佛,不過(guò)目前還是有一些遺憾和不足的。

  • @FetchRequest的控制選項(xiàng)太少
    當(dāng)前我們無(wú)法設(shè)置FetchRequest的limitNumber以及returnsObjectsAsFaults昧辽,它會(huì)直接將所有的數(shù)據(jù)讀入到上下文中衙熔,當(dāng)數(shù)據(jù)量較大時(shí),這樣的效率是很低下的搅荞。所以如果需要處理較大數(shù)據(jù)集的時(shí)候红氯,最好不要依賴(lài)@FetchRequest。
  • animation有些神經(jīng)刀
    在List中顯示@FetchRquest獲取的數(shù)據(jù)集咕痛,即使你明確設(shè)置了animation(FetchRequest痢甘,以及List),并且也顯式的使用了withAnimation對(duì)所需操作強(qiáng)制動(dòng)畫(huà)調(diào)用,但動(dòng)畫(huà)并不能總?cè)缒愕念A(yù)期般實(shí)現(xiàn)茉贡。完全相同的代碼塞栅,放置在不同的地方,有時(shí)會(huì)出現(xiàn)不同的結(jié)果腔丧。
    當(dāng)通過(guò)UITableViewDiffableDataSource數(shù)據(jù)來(lái)調(diào)用自己包裝的UITableView后放椰,動(dòng)畫(huà)就不會(huì)再不可控了。希望蘋(píng)果能早點(diǎn)解決這個(gè)Bug.
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末愉粤,一起剝皮案震驚了整個(gè)濱河市砾医,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌衣厘,老刑警劉巖如蚜,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件压恒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡错邦,警方通過(guò)查閱死者的電腦和手機(jī)探赫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)撬呢,“玉大人伦吠,你說(shuō)我怎么就攤上這事∏阒ィ” “怎么了讨勤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)晨另。 經(jīng)常有香客問(wèn)我,道長(zhǎng)谱姓,這世上最難降的妖魔是什么借尿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮屉来,結(jié)果婚禮上路翻,老公的妹妹穿的比我還像新娘。我一直安慰自己茄靠,他們只是感情好茂契,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著慨绳,像睡著了一般掉冶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脐雪,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天厌小,我揣著相機(jī)與錄音,去河邊找鬼战秋。 笑死璧亚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脂信。 我是一名探鬼主播癣蟋,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼狰闪!你這毒婦竟也來(lái)了疯搅?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤尝哆,失蹤者是張志新(化名)和其女友劉穎秉撇,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琐馆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年规阀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘦麸。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谁撼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滋饲,到底是詐尸還是另有隱情厉碟,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布屠缭,位于F島的核電站箍鼓,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏呵曹。R本人自食惡果不足惜款咖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奄喂。 院中可真熱鬧铐殃,春花似錦、人聲如沸跨新。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)域帐。三九已至赘被,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間俯树,已是汗流浹背帘腹。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留许饿,地道東北人阳欲。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像陋率,于是被迫代替她去往敵國(guó)和親球化。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355