For example. I have old models, like this:
class Foo:Object {
#objc dynamic var id = ObjectId.generate()
let bars = List<Bar>()
override class func primaryKey() -> String? {
return "id"
}
}
class Bar:Object {
#objc dynamic var id = ObjectId.generate()
override class func primaryKey() -> String? {
return "id"
}
}
and the new models:
class Foo:Object {
#objc dynamic var id = ObjectId.generate()
override class func primaryKey() -> String? {
return "id"
}
}
migration code:
let config = Realm.Configuration(
schemaVersion: 1,
migrationBlock: {migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.deleteData(forType: "Bar")
}
})
let realm = try! Realm(configuration: config)
When running, the error shows "Table is target of cross-table link columns".
If I ran first
let config = Realm.Configuration(
schemaVersion: 1,
migrationBlock: {migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
}
})
and then ran
let config = Realm.Configuration(
schemaVersion: 2,
migrationBlock: {migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
}
if oldSchemaVersion < 2 {
migration.deleteData(forType: "Bar")
}
})
The result worked.
Here is the question, is that a way to merge the two times migrations into one migration?
I found a solution. Just apply the two migrations together.
let url = Realm.Configuration().fileURL!
let schemaVersion = try! schemaVersionAtURL(url)
if schemaVersion == 0 {
autoreleasepool {
let configuration = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
}
})
_ = try! Realm(configuration: configuration)
}
autoreleasepool {
let configuration = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
}
if (oldSchemaVersion < 2) {
migration.deleteData(forType: "Bar")
}
})
_ = try! Realm(configuration: configuration)
}
} else {
let configuration = Realm.Configuration(schemaVersion: schemaVersion)
_ = try! Realm(configuration: configuration)
}
The autoreleasepool parts are necessary. Or schemaVersion 2 won't be applied.
Related
I'm having memory problems with the Swift code below.
In GameScene's sceneDidLoad(), I add a GKEntity to the array "entities," but in GameScene's update(), the array "entities" is empty. Shouldn't the reference to "entity" be preserved in "entities"? Why is "entity" being deinitialized at the end of sceneDidLoad()?
import GameplayKit
class GraphicsComponent: GKComponent {
override func update(deltaTime: TimeInterval) {
// ...
}
}
class GameScene: SKScene {
var entities = [GKEntity]()
override func sceneDidLoad() {
let entity = GKEntity()
entity.addComponent(GraphicsComponent())
entities.append(entity)
}
override func update(_ currentTime: TimeInterval) {
// ...
for entity in entities {
entity.update(deltaTime: dt)
}
}
}
I was expecting the "entity" instance to be preserved because a reference to it was added to the "entities" array. If that reference remained, then why was the instance destroyed?
EDIT:
I found that if I move the code in sceneDidLoad() to didMove(), everything works as expected. I still didn't know why, so I looked around in the boilerplate that Xcode puts in GameViewController and found where sceneDidLoad() and didMove() are each called. I found that if I add an entity to the scene.entities array in sceneDidLoad(), the scene.entities array will be empty when the code steps into the if statement that begins with if let scene = GKScene(fileNamed: "GameScene"). Is the if let creating a temporary GKScene?
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// This "if let" will call sceneDidLoad(), which appends a new entity to scene.entities.
if let scene = GKScene(fileNamed: "GameScene") {
// scene.entities.count = 0
if let sceneNode = scene.rootNode as! GameScene? {
sceneNode.entities = scene.entities
sceneNode.graphs = scene.graphs
sceneNode.scaleMode = .aspectFill
if let view = self.view as! SKView? {
view.presentScene(sceneNode) // This calls didMove()
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
}
}
This is my data structure
struct SPPWorkout: Codable {
static let setKey = "Sets"
static let exerciseID = "id"
var id: Double? = 0.0
var duration: String?
var calories: Int?
var date: String?
var exercises: [ExerciseSet]
[...]
}
struct ExerciseSet: Codable {
let id: String
let name: String
var reps: Int
var weight: Double
[...]
}
extension ExerciseSet: Equatable {
static func ==(lhs: ExerciseSet, rhs: ExerciseSet) -> Bool {
return lhs.id == rhs.id
}
}
and in a SwiftUI view I'm trying to modify an ExerciseSet from user input
#State private var sppWorkout: SPPWorkout!
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 0.0
self.editingIndex = nil
})
}
The issue is here
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ??
and I've tried in all ways to update it, both from the view and with a func in SPPWorkout. I've also tried to replace the object at index
var newSet = ExerciseSet(id: [...], newValues)
self.exercises[editingIndex] = newSet
but in no way it wants to update. I'm sure that somewhere it creates a copy that it edits but I have no idea why and how to set the new values.
Edit: if I try to delete something, it's fine
sppWorkout.exercises.removeAll(where: { $0 == sppWorkout.exercises[index]})
Edit 2:
It passes the guard statement and it does not change the values in the array.
Edit 3:
At the suggestion below from Jared, I've copied the existing array into a new one, set the new values then tried to assign the new one over to the original one but still, it does not overwrite.
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
print(sppWorkout.exercises)
guard let editingIndex = editingIndex else { return }
var copyOfTheArray = sppWorkout.exercises
copyOfTheArray[editingIndex].reps = Int(reps) ?? 0
copyOfTheArray[editingIndex].weight = Double(weight) ?? 0.0
//Copy of the array is updated correctly, it has the new values
sppWorkout.exercises = copyOfTheArray
//Original array doesn't get overwritten. It still has old values
self.editingIndex = nil
Edit 4: I've managed to make progress by extracting the model into a view model and updating the values there. Now the values get updated in sppWorkout, but even though I call objectWillChange.send(), the UI Update doesn't trigger.
full code:
class WorkoutDetailsViewModel: ObservableObject {
var workoutID: String!
#Published var sppWorkout: SPPWorkout!
func setupData(with workoutID: String) {
sppWorkout = FileIOManager.readWorkout(with: workoutID)
}
func update(_ index: Int, newReps: Int, newWeight: Double) {
let oldOne = sppWorkout.exercises[index]
let update = ExerciseSet(id: oldOne.id, name: oldOne.name, reps: newReps, weight: newWeight)
sppWorkout.exercises[index] = update
self.objectWillChange.send()
}
}
struct WorkoutDetailsView: View {
var workoutID: String!
#StateObject private var viewModel = WorkoutDetailsViewModel()
var workout: HKWorkout
var dateFormatter: DateFormatter
#State private var offset = 0
#State private var isShowingOverlay = false
#State private var editingIndex: Int?
#EnvironmentObject var settingsManager: SettingsManager
#Environment(\.dismiss) private var dismiss
var body: some View {
if viewModel.sppWorkout != nil {
VStack {
ListWorkoutItem(workout: workout, dateFormatter: dateFormatter)
.padding([.leading, .trailing], 10.0)
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
let index = viewModel.sppWorkout.exercises.firstIndex(of: exercise) ?? 0
DetailListSetItem(exerciseSet: viewModel.sppWorkout.exercises[index], set: index + 1)
.environmentObject(settingsManager)
.swipeActions {
Button(role: .destructive, action: {
viewModel.sppWorkout.exercises.removeAll(where: { $0 == viewModel.sppWorkout.exercises[index]})
} ) {
Label("Delete", systemImage: "trash")
}
Button(role: .none, action: {
isShowingOverlay = true
editingIndex = index
} ) {
Label("Edit", systemImage: "pencil")
}.tint(.blue)
}
}
.padding([.leading, .trailing], -30)
//iOS 16 .scrollContentBackground(.hidden)
}
.overlay(alignment: .bottom, content: {
editOverlay
.animation(.easeInOut (duration: 0.5), value: isShowingOverlay)
})
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
do {
try FileIOManager.write(viewModel.sppWorkout, toDocumentNamed: "\(viewModel.sppWorkout.id ?? 0).json")
} catch {
Debugger.log(error: error.localizedDescription)
}
dismiss()
}){
Image(systemName: "arrow.left")
})
} else {
Text("No workout details found")
.italic()
.fontWeight(.bold)
.font(.system(size: 35))
.onAppear(perform: {
viewModel.setupData(with: workoutID)
})
}
}
#ViewBuilder private var editOverlay: some View {
if isShowingOverlay {
ZStack {
Button {
isShowingOverlay = false
} label: {
Color.clear
}
.edgesIgnoringSafeArea(.all)
VStack{
Spacer()
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
print(viewModel.sppWorkout.exercises)
print("dupa aia:\n")
viewModel.update(editingIndex, newReps: Int(reps) ?? 0, newWeight: Double(weight) ?? 0.0)
print(viewModel.sppWorkout.exercises)
self.editingIndex = nil
})
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color("popupBackground"),
lineWidth: 3)
)
}
}
}
}
}
So I got a very good explanation on reddit on what causes the problem. Thank you u/neddy-seagoon if you are reading this.
The explanation
. I believe that updating an array will not trigger a state update. The only thing that will, with an array, is if the count changes. So
sppWorkout.exercises[index].reps = newReps
will not cause a trigger. This is not changing viewModel.sppWorkout.exercises.indices
So all I had to to was modify my List from
List(viewModel.sppWorkout.exercises, id: \.id)
to
List(viewModel.sppWorkout.exercises, id: \.hashValue)
as this triggers the list update because the hashValue does change when updating the properties of the entries in the list.
For the line
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
Replace with
List(viewModel.sppWorkout.exercises, id: \.self) { exercise in
I have a Swift playground file where I have a list of factories with child entities called engines; where a player can hold many engines
The relationship is thus;
Factory -> Engine <- Player
One factory has many engines
One player has many engines
One engine has a single parent (factory)
One engine has a single owner (player)
I'm trying to build a Swift map and filter where, it produces a list of all factories that are not owned by a given player object.
My code now follows;
//: Swift Playground code
import Foundation
class Factory : CustomStringConvertible {
var name: String = ""
var engines: [Engine] = [Engine]()
var description: String {
return self.name
}
var owners: [Player]? {
let filtered = self.engines.filter { (eng:Engine) -> Bool in
return (eng.owner != nil)
}
.sorted { (engA:Engine, engB:Engine) -> Bool in
return ((engA.owner?.turnOrder)! < (engB.owner?.turnOrder)!)
}.flatMap { (eng:Engine) -> Player? in
return (eng.owner)
}
return filtered
}
init(name: String) {
self.name = name
// create 3 children (engines)
for _ in 1...3 {
let engine = Engine.init(parent: self)
self.engines.append(engine)
}
}
}
class Engine : CustomStringConvertible {
weak var parent: Factory?
weak var owner: Player?
var description: String {
guard let hasParent = self.parent else {
return "N/A"
}
return ("\(hasParent.name) - engine")
}
init(parent: Factory) {
self.parent = parent
}
func setOwner(owner: Player) {
self.owner = owner
self.owner?.addEngine(engine: self)
}
}
class Player : CustomStringConvertible {
var name: String = ""
var engines: [Engine] = [Engine]()
var turnOrder: Int = 0
var description: String {
return self.name
}
init(name: String) {
self.name = name
}
func addEngine(engine: Engine) {
self.engines.append(engine)
}
}
// create 3 factories
let f1 = Factory.init(name: "f1")
let f2 = Factory.init(name: "f2")
let f3 = Factory.init(name: "f3")
let factories = [f1,f2,f3]
let p1 = Player.init(name: "Bob")
if let firstEngine = f1.engines.first {
firstEngine.setOwner(owner: p1)
}
print ("All factories: \(factories)")
print ("p1 = \(p1.name), engines: \(p1.engines)")
for (index, f) in factories.enumerated() {
print ("#\(index), Owners: \(f.owners)")
}
// filter all factories NOT owned by player
let filtered = factories.map({ $0.engines.filter({ (eng: Engine) -> Bool in
return (eng.owner != nil)
})})
print ("factories not owned by player")
print (filtered)
The output is as follows:
All factories: [f1, f2, f3]
p1 = Bob, engines: [f1 - engine]
#0, Owners: Optional([Bob])
#1, Owners: Optional([])
#2, Owners: Optional([])
factories not owned by player
[[f1 - engine], [], []]
The issue I'm having is the last filter code;
// filter all factories NOT owned by player
let filtered = factories.map({ $0.engines.filter({ (eng: Engine) -> Bool in
return (eng.owner != nil)
})})
This only returns factories where engines are not nil,
I wish to use:
return (eng.owner != p1)
But an error is returned;
error: cannot convert value of type 'Player' to expected argument type '_OptionalNilComparisonType'
return (eng.owner != p1)
I'm thus wondering, how can I filter a map of factories and only return a list of all factories where a given player does not own?
Many thanks
I think this is what you're looking for:
extension Factory {
func isOwned(by player: Player?) -> Bool {
return self.engines.contains(where: { $0.isOwned(by: player)} )
}
}
extension Engine {
func isOwned(by player: Player?) -> Bool {
return self.owner === player
}
}
let factoriesNotOwnedByP1 = factories.filter { !$0.isOwned(by: p1) }
And here are some other changes I would make to your existing code:
import Foundation
class Factory {
let name: String
var engines = [Engine]()
init(name: String) {
self.name = name
self.engines += (1...3).map{ _ in Engine(parent: self) }
}
var owners: [Player]? {
return self.engines
.lazy
.flatMap { engine in engine.owner.map{ (engine: engine, owner: $0) } }
.sorted { $0.owner.turnOrder < $1.owner.turnOrder }
.map { $0.owner }
}
func isOwned(by player: Player?) -> Bool {
return self.engines.contains(where: { $0.isOwned(by: player)} )
}
}
extension Factory: CustomStringConvertible {
var description: String { return self.name }
}
class Engine {
weak var parent: Factory?
weak var owner: Player?
init(parent: Factory) {
self.parent = parent
}
func setOwner(owner: Player) {
self.owner = owner
self.owner?.addEngine(engine: self)
}
}
extension Engine: CustomStringConvertible {
var description: String {
guard let parent = self.parent else { return "N/A" }
return ("\(parent.name) - engine")
}
}
class Player {
let name: String
var engines = [Engine]()
var turnOrder = 0
init(name: String) {
self.name = name
}
func addEngine(engine: Engine) {
self.engines.append(engine)
}
}
extension Player: CustomStringConvertible {
var description: String { return self.name }
}
let factories = [
Factory(name: "f1"),
Factory(name: "f2"),
Factory(name: "f3"),
]
let p1 = Player(name: "Bob")
factories.first?.engines.first?.setOwner(owner: p1)
print ("All factories: \(factories)")
print ("p1 = \(p1.name), engines: \(p1.engines)")
for (index, f) in factories.enumerated() {
print("#\(index), Owners: \(String(describing: f.owners))")
}
let factoriesNotOwnedByP1 = factories.filter { !$0.isOwned(by: p1) }
print("factories not owned by player: ")
print(factoriesNotOwnedByP1)
You need to tell swift what type .owner is:
let filtered = factories.map({ $0.engines.filter({ (eng: Engine) -> Bool in
return (eng.owner as? Player != p1)
})})
Edit: It might actually be the 'p1' that needs to be set as Player, the error message doesn't specify.
I am new to Swift and i'm trying to mess around with some UITableViews and Arrays.
I have an array of type [Day], named daysArray.
I'm initiating the array in ViewDidLoad(), daysArray has 7 "days" in it.
Also, I have UITableView, which is being populated with the daysArray.
When I'm trying to change one day (one element in the array), the whole daysArray days are changed and consequently, all cells in the UITableView are the same.
This is so peculiar, I really don't know what is wrong (:
The Day class:
import Foundation
class Day {
private var _name: String!
private var _starts: Double!
private var _ends: Double!
var name: String! {
get {
if _name == nil {
_name = "Sunday"
}
return _name
}
set {
_name = newValue
}
}
var starts: Double! {
get {
if _starts == nil {
_starts = 8.00
}
return _starts
}
set {
_starts = newValue
}
}
var ends: Double! {
get {
if _ends == nil {
_ends = 20.00
}
return _ends
}
set {
_ends = newValue
}
}
init(dayDict: Dictionary<String, AnyObject>) {
if let dictName = dayDict["name"] as! String! {
self._name = dictName
}
if let dictIsWorking = dayDict["isWorking"] as! Bool! {
self._isWorking = dictIsWorking
}
if let dictStarts = dayDict["starts"] as! Double! {
self._starts = dictStarts
}
if let dictEnds = dayDict["ends"] as! Double! {
self._ends = dictEnds
}
}
}
The code that seems to be problematic is:
import UIKit
let GeneralDayDict: [String : AnyObject] = ["name" : "Sunday" as AnyObject, "starts": 8.00 as AnyObject, "ends" : 20.00 as AnyObject]
let someDay: Day = Day(dayDict: GeneralDayDict)
class ViewController: UIViewController {
var daysArray: [Day]! = []
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...6 {
let generalDay: Day = someDay
daysArray.append(generalDay)
}
changeArray()
// Do any additional setup after loading the view, typically from a nib.
}
func changeArray() {
daysArray[5].starts = 6.00
for day in daysArray {
print("Each day in the array starts at: ", day.starts)
}
}
}
The print command in changeArray prints this:
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
As I said, very very peculiar...
Thank you for just reading my question, and also for answering of course (:
In your loop you instantiate objects with the same reference.
You have to replace :
let generalDay: Day = someDay
by this :
let generalDay = Day(dayDict: GeneralDayDict)
Then you will be able to change attributes of generalDay individually
Classes in Swift are passed by reference. Meaning each item in your array points to the same thing. So when you update one, you update them all.
Try doing this with a struct instead of using a class and you will see the difference.
See this Documentation for a better explanation and the reasoning behind this.
Set the class of Day to be NSObject
class Day : NSObject {
}
for i in 0...6 {
let generalDay: Day = someDay
daysArray.append(generalDay.copy())
}
then just append the copy of the generalDay Object
Hope it helps!!
I made table view with friends contact info.
And Each cell has button if touched,
I want to insert the info to selected friend array
(by the array, I made another small view to slide up with the friends list).
But If user the button one more,
I want to delete the friend Info in the selected friend array.
I know how to append to array,
but I don't know how to erase the specific item(NSObject) in array
by not using index.
my source code is below
class FriendModel : NSObject {
dynamic var index : ""
dynamic var thumbnail : ""
dynamic var name : ""
}
and In view controller class,
var selectedList = [FriendModel]()
#IBAction func SelectAct(_ sender: Any) {
let chooseBtn = sender as! UIButton
let indexPath = NSIndexPath(row: chooseBtn.tag, section:0)
let cell = tableView.cellForRow(at: indexPath as IndexPath) as! FriendsListSendCell
// when selected button is pushed
if chooseBtn.isSelected == true {
chooseBtn.isSelected = false
count = count! - 1
if self.count! < 1 {
self.windowShowUp = false
UIView.animate(withDuration: 0.1, delay: 0.1, options: [], animations:{self.selectedBoard.center.y += 80 }, completion: nil)
self.checkNumLabel.text = ""
}else{
}
//////////////////////////here////////////////////////////
//////////////////how to erase the FriendModel(NSObject) in selectedList.
}
//when the unselected button is pushed
else {
//instance for append the friend info
let friendInfo = FriendModel()
chooseBtn.isSelected = true
count = count! + 1
friendInfo.thumbnail = cell.thumbnail
friendInfo.name = cell.nameLabel.text!
//add friend info to selectedList
self.selectedList.append(friendInfo)
print("\(self.selectedList)")
if self.windowShowUp! == false{
self.windowShowUp = true
UIView.animate(withDuration: 0.1, delay: 0.1, options: [], animations:{self.selectedBoard.center.y -= 80 }, completion: nil)
}else{
}
}
}
You can use index(where:) to get index of your object and then remove item at index position.
class FriendModel {
var index = ""
var thumbnail = ""
var name = ""
}
let fm0 = FriendModel()
fm0.index = "100"
fm0.thumbnail = "tell.png"
fm0.name = "Phillips"
let fm1 = FriendModel()
fm1.index = "200"
fm1.thumbnail = "ask.png"
fm1.name = "Allen"
var array = [FriendModel]()
array.append(fm0)
array.append(fm1)
// The index below is an index of the array. Do not confuse with the FriendModel.index
let index = array.index {
return $0.thumbnail == "ask.png" && $0.name == "Allen"
}
array.forEach { print($0.name) }
print("Array index:", index ?? "n/a")
array.remove(at: index!)
array.forEach { print($0.name) }
You could use MirekE's solution which would work but here's an alternative solution.
First step would be identifying what the unique identifier on your FriendModel object is such as an id property.
Then on your model, since it is an NSObject, override the isEqual function like this:
override public func isEqual(object: AnyObject?) -> Bool {
let friend = object as? FriendModel
if self.id == friend?.id { return true }
return false
}
At this point you can use your desired form of iteration to find your element in the array and use remove.