rhx / SwiftGtk

A Swift wrapper around gtk-3.x and gtk-4.x that is largely auto-generated from gobject-introspection
https://rhx.github.io/SwiftGtk/
BSD 2-Clause "Simplified" License
317 stars 26 forks source link

cannot convert value of T to expected argument type of Value #53

Closed bryceac closed 2 years ago

bryceac commented 2 years ago

Hello, I am currently playing around with SwiftGtk, trying to port an application over to Linux, and I am hitting a real snag here.

I am trying to make a ListView, so I am following the code as seen here, modifying as needed, which is currently loading objects from a JSON file, and when I get to the point of hooking up the rows with the data, I get the complaint that values of variables and/or functions cannot be converted, even though they are returning the right types, yet literals work just fine.

Can you either fix this or note down how exactly I'm supposed to deal with this?

rhx commented 2 years ago

Are you able to provide some more details? E.g., what does your modified code look like, and what is the exact error message you are getting (i.e. what is the line in your code that is causing the issue)?

bryceac commented 2 years ago

The code from when I tried looks like this, though the code looks more like your code on the github page.

let TEST_FILE = URL(fileURLWithPath: "~/transactions.bcheck").standardizedFileURL

if let STORED_RECORDS = try? Record.load(from: TEST_FILE) {
    for record in STORED_RECORDS {
        Records.shared.add(record)
    }
}

let status = Application.run(startupHandler: nil) { app in
    let window = ApplicationWindowRef(application: app)
    window.title = "Hello, World!"
    window.setDefaultSize(width: 320, height: 240)
    /* let label = LabelRef(str: "Hello, World!")
    window.add(widget: label)
    window.showAll() */
    let iterator = TreeIter()
    let store = ListStore(.string, .string, .boolean, .string, .string, .string, .string, .string)
    var listView = ListView(model: store)
    let columns = [
        ("Date", "text", CellRendererText()),
        ("Check #", "text", CellRendererText()),
        ("Reconciled", "active", CellRendererToggle()),
        ("Vendor", "text", CellRendererText()),
        ("Memo", "text", CellRendererText()),
        ("Deposit", "text", CellRendererText()),
        ("Withdrawal", "text", CellRendererText()),
        ("Balance", "text", CellRendererText())
    ].enumerated().map {(i: Int, c:(title: String, kind: PropertyName, renderer: CellRenderer)) in
        TreeViewColumn(i, title: c.title, renderer: c.renderer, attribute: c.kind)
    }
    listView.append(columns)
    window.add(widget: listView)
    for record in Records.shared.sortedRecords {
        switch record.event.type {
            case .deposit: 
                if let checkNumber = record.event.checkNumber {
                    store.append(asNextRow: iterator, 
                    "\(Event.DF.string(from: record.event.date))", 
                    "\(checkNumber)", 
                    record.event.isReconciled ? true : false,
                    "\(record.event.vendor)",
                    "\(record.event.memo)",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.event.amount))!)",
                    "N/A",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.balance))!)")
                } else {
                    store.append(asNextRow: iterator, 
                    "\(Event.DF.string(from: record.event.date))", 
                    "N/A", 
                    record.event.isReconciled ? true : false,
                    "\(record.event.vendor)",
                    "\(record.event.memo)",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.event.amount))!)",
                    "N/A",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.balance))!)")
                }
            case .withdrawal:
                if let checkNumber = record.event.checkNumber {
                    store.append(asNextRow: iterator, 
                    "\(Event.DF.string(from: record.event.date))", 
                    "\(checkNumber)", 
                    record.event.isReconciled ? true : false,
                    "\(record.event.vendor)",
                    "\(record.event.memo)",
                    "N/A",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.event.amount))!)",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.balance))!)")
                } else {
                    store.append(asNextRow: iterator, 
                    "\(Event.DF.string(from: record.event.date))", 
                    "N/A", 
                    record.event.isReconciled ? true : false,
                    "\(record.event.vendor)",
                    "\(record.event.memo)",
                    "N/A",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.event.amount))!)",
                    "\(Event.CURRENCY_FORMAT.string(from: NSNumber(value: record.balance))!)")
                }
        }
    }
    window.showAll()
}

guard let status = status else {
    fatalError("Could not create Application")
}

guard status == 0 else {
    fatalError("Application exited with status \(status)")
}

As can be seen here, I tried resorting to string interpolation, which I should not need to do for things that are already String or booleans values, like the following:

DF and CURRENCY_FORMAT are used to convert values to strings, yet those also receive complaints, even when using string interpolation.

As for the models, they are made up of the following:

EventTypeError:

enum EventTypeError: LocalizedError {
    case invalidType

    var errorDescription: String? {
        var error: String? = nil

        switch self {
        case .invalidType: error = "Specified type is not valid."
        }

        return error
    }
}

EventType:

enum EventType: String, CaseIterable {
    case deposit, withdrawal
}

extension EventType: Codable {
    init(from decoder: Decoder) throws {
        let CONTAINER = try decoder.singleValueContainer()

        let TYPE_STRING = try CONTAINER.decode(String.self)

        guard let TYPE = EventType.allCases.first(where: { $0.rawValue.caseInsensitiveCompare(TYPE_STRING) == .orderedSame }) else {
            throw EventTypeError.invalidType
        }

        self = TYPE
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        try container.encode(self.rawValue)
    }
}

Event:

struct Event {
    var date: Date = Date()
    var checkNumber: Int? = nil
    var vendor: String = ""
    var memo: String = ""
    var amount: Double = 0
    var type: EventType = .withdrawal
    var isReconciled: Bool = false

    static let DF: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter
    }()

    static let CURRENCY_FORMAT: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()
}

extension Event: Codable {
    private enum CodingKeys: String, CodingKey {
        case date, checkNumber = "check_number", vendor, memo, amount, type, isReconciled = "is_reconciled"
    }

    init(from decoder: Decoder) throws {
        let CONTAINER = try decoder.container(keyedBy: CodingKeys.self)

        if CONTAINER.contains(.date) {
            let DATE_STRING = try CONTAINER.decode(String.self, forKey: .date)
            if let TRANSACTION_DATE = Event.DF.date(from: DATE_STRING) {
                date = TRANSACTION_DATE
            }
        }

        if CONTAINER.contains(.checkNumber) {
            checkNumber = try CONTAINER.decode(Int.self, forKey: .checkNumber)
        }

        vendor = try CONTAINER.decode(String.self, forKey: .vendor)

        if CONTAINER.contains(.memo) {
            memo = try CONTAINER.decode(String.self, forKey: .memo)
        }

        amount = try CONTAINER.decode(Double.self, forKey: .amount)

        type = try CONTAINER.decode(EventType.self, forKey: .type)

        if CONTAINER.contains(.isReconciled) {
            isReconciled = try CONTAINER.decode(Bool.self, forKey: .isReconciled)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(Event.DF.string(from: date), forKey: .date)

        if let checkNumber = checkNumber {
            try container.encode(checkNumber, forKey: .checkNumber)
        }

        try container.encode(vendor, forKey: .vendor)

        if !memo.isEmpty {
            try container.encode(memo, forKey: .memo)
        }

        try container.encode(amount, forKey: .amount)

        try container.encode(type, forKey: .type)

        if isReconciled {
            try container.encode(isReconciled, forKey: .isReconciled)
        }
    }
}

extension Event: Comparable {
    static func ==(lhs: Event, rhs: Event) -> Bool {
        return lhs.date == rhs.date && lhs.checkNumber == rhs.checkNumber && lhs.vendor == rhs.vendor && lhs.memo == rhs.memo && lhs.amount == rhs.amount && lhs.type == rhs.type && lhs.isReconciled == rhs.isReconciled
    }

    static func < (lhs: Event, rhs: Event) -> Bool {
        var isLessThanOther = false

        if let firstNumber = lhs.checkNumber, let secondNumber = rhs.checkNumber {
            isLessThanOther = lhs.date < rhs.date || firstNumber < secondNumber || lhs.vendor < rhs.vendor || lhs.amount < rhs.amount
        } else {
            isLessThanOther = lhs.date < rhs.date || lhs.vendor < rhs.vendor || lhs.amount < rhs.amount
        }

        return isLessThanOther
    }
}

extension Event: Hashable {}

Record:

class Record: Identifiable, Codable {
    let id: String
    var event: Event
    var previousRecord: Record? = nil

    var balance: Double {
        var value = previousRecord?.balance ?? 0

        switch event.type {
        case .deposit: value += event.amount
        case .withdrawal: value -= event.amount
        }

        return value
    }

    private enum CodingKeys: String, CodingKey {
        case id, event = "transaction"
    }

    init(withID id: String = UUID().uuidString, transaction: Event = Event(), previousRecord: Record? = nil) {
        (self.id, self.event, self.previousRecord) = (id, transaction, previousRecord)
    }

    required convenience init(from decoder: Decoder) throws {
        let CONTAINER = try decoder.container(keyedBy: CodingKeys.self)

        let ID = CONTAINER.contains(.id) ? try CONTAINER.decode(String.self, forKey: .id) : UUID().uuidString

        let TRANSACTION = try CONTAINER.decode(Event.self, forKey: .event)

        self.init(withID: ID, transaction: TRANSACTION)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(id, forKey: .id)
        try container.encode(event, forKey: .event)
    }

    class func load(from path: URL) throws -> [Record] {
        let JSON_DECODER = JSONDecoder()

        let RECORD_DATA = try Data(contentsOf: path)
        let DECODED_RECORDS = try JSON_DECODER.decode([Record].self, from: RECORD_DATA)

        return DECODED_RECORDS
    }
}

extension Record: Comparable {
    static func ==(lhs: Record, rhs: Record) -> Bool {
        return lhs.id == rhs.id
    }

    static func < (lhs: Record, rhs: Record) -> Bool {
        return lhs.id < rhs.id || lhs.event < rhs.event
    }
}

extension Record: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(event)
    }
}

extension Array where Element == Record {
    func save(to path: URL) throws {
        let JSON_ENCODER = JSONEncoder()
        JSON_ENCODER.outputFormatting = .prettyPrinted

        let ENCODED_RECORDS = try JSON_ENCODER.encode(self)

        #if os(iOS)
        try ENCODED_RECORDS.write(to: path, options: .noFileProtection)
        #else
        try ENCODED_RECORDS.write(to: path, options: .atomic)
        #endif
    }

    func element(before record: Record) -> Record? {
        guard self.first! != record else { return nil }
        guard let INDEX = self.firstIndex(of: record) else { return nil }

        let PREVIOUS_INDEX = self.index(before: INDEX)

        return self[PREVIOUS_INDEX]
    }

    func element(after record: Record) -> Record? {
        guard self.last! != record else { return nil }
        guard let INDEX = self.firstIndex(of: record) else { return nil }

        let NEXT_INDEX = self.index(after: INDEX)

        return self[NEXT_INDEX]
    }
}

Records:

class Records {
    static let shared = Records()
    var items: [Record] {
        didSet {
            sortedRecords.forEach { record in
                record.previousRecord = items.element(before: record)
            }
        }
    }

    var sortedRecords: [Record] {
        return items.sorted { firstRecord, secondRecord in
            firstRecord.event.date < secondRecord.event.date
        }
    }

    private init(withRecords records: [Record] = []) {
        items = records

        guard !records.isEmpty && records.count > 1 else { return }

        for index in records.indices where index != records.startIndex {
            let PREVIOUS_INDEX = records.index(before: index)

            records[index].previousRecord = records[PREVIOUS_INDEX]
        }
    }

    func add(_ record: Record) {
        items.append(record)
    }

    func remove(at index: Int) {
        items.remove(at: index)
    }

    func clear() {
        items.removeAll()
    }

    func element(matching record: Record) -> Record? {
        guard items.contains(record) else { return nil }

        return items.first(where: { $0 == record })
    }
}

Records was one I had to make for Mac/iOS and since I could only compile the GTK stuff in Debian 10 with Swift 5.4, I had to turn it into a singleton from a ObservableObject, though I was considering getting rid of it entirely.

The project itself, since I made it in Debian 10, I did what the usage instructions in the README told me to, rather than cloning one of the examples.

As for the error message, it is pretty much the same as the title, as T is just a stand in.

On String likes the code retrieving the date and getting dollar amounts, it says, "cannot convert value of String to expected argument type of Value," which is also the same complaint that crop up when vendor and memo are retrieved outside of interpolation.

On the code for retrieving isReconciled without the ternaries, which should not be needed, it says, "cannot convert value of Bool to expected argument type of Value."

The JSON file, with the extension seen in the above code, itself contains the following:

[
  {
    "id" : "FF04C3DC-F0FE-472E-8737-0F4034C049F0",
    "transaction" : {
      "amount" : 500,
      "vendor" : "Sam Hill Credit Union",
      "memo" : "Open Account",
      "check_number" : 1260,
      "type" : "deposit",
      "date" : "2021-07-08"
    }
  },
  {
    "id" : "1422CBC6-7B0B-4584-B7AB-35167CC5647B",
    "transaction" : {
      "amount" : 200,
      "vendor" : "Fake Street Electronics",
      "memo" : "Head set",
      "type" : "withdrawal",
      "date" : "2021-07-08"
    }
  },
  {
    "id" : "BB22187E-0BD3-41E8-B3D8-8136BD700865",
    "transaction" : {
      "amount" : 50000,
      "vendor" : "Velociraptor Entertainment",
      "memo" : "Pay Day",
      "type" : "deposit",
      "date" : "2021-07-08"
    }
  }
]

I hope this provides you with all the details you need.

rhx commented 2 years ago

if you look at the documentation for the append(asNextRow:...) ListStore method, you can see that the parameters are Values, which are ExpressibleByStringLiteral, so any initialisation with a double-quoted string (such as the String interpolation in your example) just works.

If you want to initialise a value from a string variable (without going through a String literal), you can just pass the string to the corresponding initialiser, i.e. use Value(myString). So something like Value(Event.DF.string(from: record.event.date) should work.

bryceac commented 2 years ago

I'll give that a try thanks.

On Wed, Jul 28, 2021, 1:02 AM Rene Hexel @.***> wrote:

if you look at the documentation for the append(asNextRow:...) https://rhx.github.io/SwiftGtk3Doc/Classes/ListStore.html#/s:3Gtk9ListStoreC6append9asNextRow11startColumn_yx_Si10GLibObject5ValueCdtAA16TreeIterProtocolRzlF ListStore method, you can see that the parameters are Value https://rhx.github.io/SwiftGObject/Classes/Value.htmls, which are ExpressibleByStringLiteral https://developer.apple.com/documentation/swift/expressiblebystringliteral, so any initialisation with a double-quoted string (such as the String interpolation in your example) just works.

If you want to initialise a value from a string variable (without going through a String literal), you can just pass the string to the corresponding initialiser, i.e. use Value(myString) https://rhx.github.io/SwiftGObject/Classes/Value.html#/s:10GLibObject5ValueCyACxSgclufc. So something like Value(Event.DF.string(from: record.event.date) should work.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/rhx/SwiftGtk/issues/53#issuecomment-888101452, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHKMOW3F6TRFONYX2GSSVLTZ62QPANCNFSM5BCY2YSA .

bryceac commented 2 years ago

Thanks for the links. I fired up Debian today and wrapping them up in Value() worked, like how you suggested.

The date string was definitely complaining even when I used string interpolation, so that fixed that, though the check number started complaining to, so I did the same thing and it looks like the error is gone.

Still kind of annoying to have to do it this way, but that helps explain things not presented by my text Editor, which is VSCodium in the Linux environment, thanks to finding out somebody is distributing a prebuilt variant of the VS Code plugin.

Thanks again for the help.