Is it possible to support moving rows in a SwiftUI Table view?
I know there's List which can optionally support selection and drag-and-drop to move single or multiple rows. Since it seems similar, I would like to do this with a Table too, but I wasn't able to find any way to do this. Is this possible in SwiftUI? And if it is, what's the best way to do it?
Where I started to figure this out was the WWDC 2021 session "SwiftUI on the Mac: Finishing Touches". I highly recommend this video, as well as the preceding one "SwiftUI on the Mac: Build the Fundamentals". The code for both sessions is available.
Since you didn't include your code to show what you want to do, I have to use my code. I have a table based on an array of an Identifiable struct called Channel. Among a number of fields which are irrelevant to this problem, there is a field "id" of type UUID.
Following the model of the WWDC video, I made an extension to Channel:
import UniformTypeIdentifiers
extension Channel {
static var draggableType = UTType(exportedAs: "com.yourCompany.yourApp.channel")
// define your own type here. don't forget to include it in your info.plist as an exported type
static func fromItemProviders(_ itemProviders: [NSItemProvider], completion: #escaping ([Channel]) -> Void) {
let typeIdentifier = Self.draggableType.identifier
let filteredProviders = itemProviders.filter {
$0.hasItemConformingToTypeIdentifier(typeIdentifier)
}
let group = DispatchGroup()
var result = [Int: Channel]()
for (index, provider) in filteredProviders.enumerated() {
group.enter()
provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { (data, error) in
defer { group.leave() }
guard let data = data else { return }
let decoder = JSONDecoder()
guard let channel = try? decoder.decode(Channel.self, from: data)
else { return }
result[index] = channel
}
}
group.notify(queue: .global(qos: .userInitiated)) {
let channels = result.keys.sorted().compactMap { result[$0] }
DispatchQueue.main.async {
completion(channels)
}
}
}
var itemProvider: NSItemProvider {
let provider = NSItemProvider()
provider.registerDataRepresentation(forTypeIdentifier: Self.draggableType.identifier, visibility: .all) {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(self)
$0(data, nil)
} catch {
$0(nil, error)
}
return nil
}
return provider
}
}
This makes an item in the table draggable. Of course, that does no good if there's nothing that will accept the drag. So, you have to make a change to your Table.
Table(selection: $selection, sortOrder: $sortOrder) {
// for clarity, I've removed the table columns
} rows: {
ForEach(document.channels) { channel in
TableRow(channel)
.itemProvider { channel.itemProvider }
}
.onInsert(of: [Channel.draggableType]) { index, providers in
Channel.fromItemProviders(providers) { channels in
document.channels.insert(contentsOf: channels, at: newIndex)
}
}
}
}
Now that will enable you to drag item or items from one window to another. You can, of course, drag within a table now, too. Unfortunately, you will end up making a copy in the new place. Not what you want to do in most cases. How to fix this? Delete the original copy! Of course, you can also run into the problem of indexing in the right place, and if you drag more than one item (from a discontinuous selection, even worse!), the results become, shall we say, undefined.
I still wanted to be able to drag multiple items from another table, so the final onInsert becomes a little more complex (Which I'm sure could be cleaned up a bot further):
Channel.fromItemProviders(providers) { channels in
var newIndex = index
let intraTableDrag = document.channels.contains(where: {$0.id == channels[0].id})
if intraTableDrag && channels.count == 1 {
let oldIndex = document.channels.firstIndex(where: {$0.id == channels[0].id})
if newIndex > oldIndex! {
newIndex -= 1
}
for channel in channels {
let channelID = channel.id
removeChannel(withID: channelID)
}
let maxIndex = document.channels.count
if index > maxIndex {
newIndex = maxIndex
}
}
if (intraTableDrag && channels.count == 1) || !intraTableDrag {
document.channels.insert(contentsOf: channels, at: newIndex)
document.setChannelLocationToArrayOrder()
}
}
}
I hope this is enough to get you started. Good luck!
Related
This function is not acting as expected. I'm trying to nil a set of fields.The earlier section gets the correct field names, and is used in other functions. I've got about ten tables, and they all share the same context, in case that matters.
The first unexpected thing is that "yes, changes" never runs, so I presume that the settings object is detached from its context. Or perhaps CoreData treats nil as some kind of exception to triggering the .hasChanges flag?
When it runs, the save throws no errors, and the object displays as expected, displayed with the values set to nil. But there are no changes in the db.
I can save data into these fields without problem, and confirm that in the db; this problem only happens with setting the value to nil.
static func clearSettings(_ s : Set<PaletteElementType>? = nil) {
guard var setting = activeSetting() else {return}
print(setting.id)
let cats = s ?? PaletteView.compCatButtons
let tgts = setting.getAssociatedFieldNames(tgts: cats, clearing : true, values: false)
for (key, val) in tgts {
var src = Set((val as? Dictionary<FieldNameSuffixes, String>)!.values)
if key == .catBgndButton {
src = src.union(["opacity", "opacityStart", "opacityStartDelta","opacityEnd", "opacityEndDelta", "opacityTimer"])
}
for s in src {
print(s)
setting.setNilValueForKey(s)
if Blocks.context!.hasChanges {
print("yes, changes")
}
do {
try Blocks.context!.save()
print("deleted \(setting.value(forKey: s))")
} catch { print("deadly dogs")}
}
print("val is \(setting)")
}
}
OK, working when I do it this way:
static func clearSettings(_ s : Set<PaletteElementType>? = nil) {
guard var setting = activeSetting() else {return}
print(setting.id)
let cats = s ?? PaletteView.compCatButtons
let tgts = setting.getAssociatedFieldNames(tgts: cats, clearing : true, values: false)
for (key, val) in tgts {
var src = Set((val as? Dictionary<FieldNameSuffixes, String>)!.values)
if key == .catBgndButton {
src = src.union(["opacity", "opacityStart", "opacityStartDelta","opacityEnd", "opacityEndDelta", "opacityTimer"])
}
for n in setting.entity.attributesByName.enumerated() {
if src.contains( n.element.key as String) {
print("found one")
setting.setNilValueForKey(n.element.key)
}
}
do {
try Blocks.context!.save()
} catch {print("bumpy beasts")}
print("val is \(setting)")
}
}
Happy it's working, but I don't really understand the distinction here. What is the better way to handle this? I'm not chasing some super performant code, so I don't mind a few extra loops... but what's the deal?
iOS 14, Swift 5.x
I watched this excellent WWDC from 2018
https://developer.apple.com/videos/play/wwdc2018/223/
And I wrote a shapes editor... and have been trying to use partition as Dave in the video says you should. I got the first three to work, but the last one I had to use a loop- cannot for the life of me figure out how to get it to work with partition.
Can someone see how I might do this?
The first method moves the selected object to the end of the list, works perfectly.
func bringToFrontEA() {
let subset = objects.partition(by: { $0.selected })
let selected = objects[subset...]
let unselected = objects[..<subset]
let reordered = unselected + selected
objects = Array(reordered)
}
The second method moves the selected object to the front of the list. Works prefectly.
func sendToBackEA() {
let subset = objects.partition(by: { !$0.selected })
let selected = objects[subset...]
let unselected = objects[..<subset]
let reordered = unselected + selected
objects = Array(reordered)
}
The third method moves the element just one element back in the list. Works perfectly.
func sendBackEA() {
if let i = objects.firstIndex(where: { $0.selected }) {
if i == 0 { return }
let predecessor = i - 1
let shapes = objects[predecessor...].partition(by: { !$0.selected })
let slice = objects[predecessor...]
let row = objects[..<predecessor]
let selected = Array(slice[..<shapes])
let unselected = Array(slice[shapes...])
objects = row + selected + unselected
}
}
The last method moves the element forward in the list, works perfectly... but unlike the other methods it will not scale as described in the WWDC video.
func bringForwardEA() {
let indexes = objects.enumerated().filter { $0.element.selected == true }.map{$0.offset}
for i in indexes {
if objects[i+1].unused {
return
}
objects.swapAt(i+1, i)
}
}
Objects is an array of shapes with a property indicating if it is selected or not. I want to exchange the loop in the last method by using a partition as I did in the first three. It needs to work for one or more selected shapes.
Looking at the WWDC video, it appears that what you are calling sendBackEA is what WWDC calls bringForward, and what you are calling bringForwardEA is what WWDC calls sendBack.
Just like how you move the first selected element forward one index (index decreases) in sendBackEA, then move all the other selected elements to immediately after that first selected element. bringForwardEA should do the reverse: move the last selected element backward one index (index increases), then move all the other selected elements to immediately before the last selected element. (See circa 19:10 in the video)
You seem to have confused yourself by trying to increase the indices of all the selected index by 1. This obviously cannot be done with a partition in general.
Also note that partition(by:) already modifies the collection, you don't need to get each partition, then recombine.
Your 4 methods can be written like this:
func bringToFrontEA() {
objects.partition(by: { $0.selected })
}
func sendToBackEA() {
objects.partition(by: { !$0.selected })
}
func sendBackEA() {
if let i = objects.indices.first(where: { objects[$0].selected }) {
if i == 0 { return }
let predecessor = i - 1
objects[predecessor...].partition(by: { !$0.selected })
}
}
func bringForwardEA() {
if let i = objects.indices.last(where: { objects[$0].selected }) {
if i == objects.indices.last { return }
let successor = i + 1
objects[...successor].partition(by: { !$0.selected })
}
}
Notice the symmetry between sendBackEA and bringForwardEA.
I am trying to implement something like this,
let api1 = Observable.of(["documents"]) //Replace with observable to download docs
let api2 = Observable.of(["applications"]) //Replace with observable to download apps
let api3 = Observable.of(["videos"]) //Replace with observable to download videos
Observable.combineLatest(api1, api2, api3){(docs, apps, videos) in
return (docs, apps, videos)
}.skipWhile{ (docs, apps, videos) in
return docs.count == 0 && apps.count == 0 && videos.count == 0
}.subscribe(onNext:{(docs, apps, videos) in
})
.disposed(by:disposeBag)
In my case, I am trying to create observables dynamically and add it to an array like this,
private var discoverObservables = [Observable<Any>]()
func loadDiscoverFeeds(){
self.feeds.forEach({
feed in
switch feed.feedType{
case "a":
let observable = self.aObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
case "b":
let observable = self.bObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
case "c":
let observable = self.cObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
case "d" :
let observable = self.dObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
default:
break
}
})
}
private func aObservable(url : String) -> Observable<A?>{
return APIManager.shared.getA(url: url)
}
private func bObservable(url : String) -> Observable<B?>{
return APIManager.shared.getB(url: url)
}
private func cObservable(url : String) -> Observable<C?>{
return APIManager.shared.getC(url: url)
}
But this is not working because discoverObservables array is expecting the value of Type Observable<Any> and I am trying to add Observable<A?>
How can I do this correctly, I want to make sure all the observables return data before I start processing the data.
Edit
I am trying to load data from different sources before that is added to the view, basically, I have a collectionview, each section loads data from different API, I am trying to get all the required data from all sources before that is added to collection view.
Add the same protocol to A, B and C.
protocol YourProtocol {...}
class A: YourProtocol {...}
class B: YourProtocol {...}
class C: YourProtocol {...}
Then you can make :
private var discoverObservables = [Observable<YourProtocol>]()
The first code block seems to be doing the job with one exception, the condition checks if all of the (docs, apps, videos) are empty, perhaps you wanted to use || instead of &&.
As for the second code block with an array, I did something that could help.
struct A {}
let observable1 = Observable.just(A())
let observable2 = Observable.just(A())
let observable3 = Observable.just(A())
let observables: [Observable<A>] = [observable1, observable2, observable3]
Observable.combineLatest(observables).skipWhile { (streams) -> Bool in
streams.forEach {
if $0.count == 0 { return true }
}
return false
}.subscribe(...
This subscription will result with Observable<[A]>.
I'm going to specifically address this from your question: "I want to make sure all the observables return data before I start processing the data."
Strictly speaking, you probably don't want an Any structure. Better would be a protocol or enum. I see that other answers have addressed the protocol idea so I will use the enum idea:
enum EndpointResponse {
case a(A?)
case b(B?)
// etc...
}
let responses = Observable.zip(
feeds.map { (feed) -> Observable<EndpointResponse> in
switch feed.feedType {
case "a":
return aObservable(url: feed.feedURL ?? "").map { EndpointResponse.a($0) }
case "b":
return bObservable(url: feed.feedURL ?? "").map { EndpointResponse.b($0) }
default:
fatalError()
}
}
)
The above responses observable will contain an array of all the responses once they have all emitted values. In other words, the zip operator will gather up all the responses from all the network calls and emit a single array containing all of them.
My Previous answer:
There really isn't a lot of information to go on in the question, but something like this answers the direct question you ask about converting an Observable<X> to an Observable<Any>...
let discoverObservables = Observable.zip(
feeds.map { (feed) -> Observable<Any> in
switch feed.feedType {
case "a":
return aObservable(url: feed.feedURL ?? "").map { $0 as Any }
case "b":
return bObservable(url: feed.feedURL ?? "").map { $0 as Any }
case "c":
return cObservable(url: feed.feedURL ?? "").map { $0 as Any }
case "d":
return dObservable(url: feed.feedURL ?? "").map { $0 as Any }
default:
break
}
}
)
So I know that Swift Arrays are struct, so they are passed by value
Here is my code:
firstSnapshotList and secondSnapshotList are both [Snapshot]
if datePicker == firstDatePicker {
firstSnapshotList.removeAll()
if let snapshots = SnapshotList.snapshotDict[dateKey] {
for snapshot in snapshots {
firstSnapshotList.append(snapshot)
}
}
} else if datePicker == secondDatePicker {
secondSnapshotList.removeAll()
if let snapshots = SnapshotList.snapshotDict[dateKey] {
for snapshot in snapshots {
secondSnapshotList.append(snapshot)
}
}
}
so as you can see, both blocks if the if statements are the same, except for the array being modified.
I would love to declare a variable first, var snapshotList = *snapshot reference*,
and then if I could set that reference to either firstSnapshotList or secondSnapshotList depending on the Date Picker, then my code would just be
if datePicker == firstDatePicker {
snapshotList = firstSnapshotList
} else if datePicker == secondDatePicker {
snapshotList = secondSnapshotList
}
snapshotList.removeAll()
if let snapshots = SnapshotList.snapshotDict[dateKey] {
for snapshot in snapshots {
snapshotList.append(snapshot)
}
}
You could argue that the number of lines is the same, so it's not really any more readable or maintainable-- it's just personal preference for me, I think the second version makes more sense, mostly because there isn't any repeat code
But if this is just part of the Swift lifestyle, I can live with it too. Just mostly wondering if it's even possible to declare a variable as a reference
As you said Swift arrays are structs and these are indeed passed as values. You could instead pass the array to a separate method as an inout argument:
func yourMethodName(_ array: inout [Snapshot]) {
array.removeAll()
if let snapshots = SnapshotList.snapshotDict[dateKey] {
for snapshot in snapshots {
array.append(snapshot)
}
}
}
Then just call the method where you need it like so:
if datePicker == firstDatePicker {
yourMethodName(&firstSnapshotList)
} else if datePicker == secondDatePicker {
yourMethodName(&secondSnapshotList)
}
Arrays, like all variable-size collections in the standard library, use copy-on-write optimization. Multiple copies of an array share the same storage until you modify one of the copies. When that happens, the array being modified replaces its storage with a uniquely owned copy of itself, which is then modified in place. Optimizations are sometimes applied that can reduce the amount of copying.
Your code could be simplified to the point where there's no need to reduce code duplication:
let newSnapshotList = SnapshotList.snapshotDict[dateKey] ?? []
if datePicker == firstDatePicker {
firstSnapshotList = newSnapshotList
} else if datePicker == secondDatePicker {
secondSnapshotList = newSnapshotList
}
But for the sake of answering the question, this can be done by declaring a nested function, within your existing function:
func overrideFromSnapshotDict(destination: inout [YourType]) {
destination.removeAll()
if let snapshots = SnapshotList.snapshotDict[dateKey] {
for snapshot in snapshots {
destination.append(snapshot)
}
}
}
if datePicker == firstDatePicker {
overrideFromSnapshotDict(destination: &firstSnapshotList)
} else if datePicker == secondDatePicker {
overrideFromSnapshotDict(destination: &secondSnapshotList)
}
I'm trying to have a handler in my Mac OS X app written in Swift for a global (system-wide) hotkey combo but I just cannot find proper documentation for it. I've read that I'd have to mess around in some legacy Carbon API for it, is there no better way? Can you show me some proof of concept Swift code? Thanks in advance!
Since Swift 2.0, you can now pass a function pointer to C APIs.
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
gMyHotKeyID.id = UInt32(keyCode)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyPressed)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {(nextHanlder, theEvent, userData) -> OSStatus in
var hkCom = EventHotKeyID()
GetEventParameter(theEvent, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, sizeof(EventHotKeyID), nil, &hkCom)
// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifierKeys), gMyHotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)
I don't believe you can do this in 100% Swift today. You'll need to call InstallEventHandler() or CGEventTapCreate(), and both of those require a CFunctionPointer, which can't be created in Swift. Your best plan is to use established ObjC solutions such as DDHotKey and bridge to Swift.
You can try using NSEvent.addGlobalMonitorForEventsMatchingMask(handler:), but that only makes copies of events. You can't consume them. That means the hotkey will also be passed along to the currently active app, which can cause problems. Here's an example, but I recommend the ObjC approach; it's almost certainly going to work better.
let keycode = UInt16(kVK_ANSI_X)
let keymask: NSEventModifierFlags = .CommandKeyMask | .AlternateKeyMask | .ControlKeyMask
func handler(event: NSEvent!) {
if event.keyCode == self.keycode &&
event.modifierFlags & self.keymask == self.keymask {
println("PRESSED")
}
}
// ... to set it up ...
let options = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionaryRef
let trusted = AXIsProcessTrustedWithOptions(options)
if (trusted) {
NSEvent.addGlobalMonitorForEventsMatchingMask(.KeyDownMask, handler: self.handler)
}
This also requires that accessibility services be approved for this app. It also doesn't capture events that are sent to your own application, so you have to either capture them with your responder chain, our use addLocalMointorForEventsMatchingMask(handler:) to add a local handler.
The following code works for me for Swift 5.0.1. This solution is the combination of the solution from the accepted answer by Charlie Monroe and the recommendation by Rob Napier to use DDHotKey.
DDHotKey seems to work out of the box but it had one limitation that I had to change: the eventKind is hardcoded to kEventHotKeyReleased while I needed both kEventHotKeyPressed and kEventHotKeyReleased event types.
eventSpec.eventKind = kEventHotKeyReleased;
If you want to handle both Pressed and Released events, just add a second InstallEventHandler call which registers the other event kind.
This the complete example of the code that registers the "Command + R" key for the kEventHotKeyReleased type.
import Carbon
extension String {
/// This converts string to UInt as a fourCharCode
public var fourCharCodeValue: Int {
var result: Int = 0
if let data = self.data(using: String.Encoding.macOSRoman) {
data.withUnsafeBytes({ (rawBytes) in
let bytes = rawBytes.bindMemory(to: UInt8.self)
for i in 0 ..< data.count {
result = result << 8 + Int(bytes[i])
}
})
}
return result
}
}
class HotkeySolution {
static
func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 {
let flags = cocoaFlags.rawValue
var newFlags: Int = 0
if ((flags & NSEvent.ModifierFlags.control.rawValue) > 0) {
newFlags |= controlKey
}
if ((flags & NSEvent.ModifierFlags.command.rawValue) > 0) {
newFlags |= cmdKey
}
if ((flags & NSEvent.ModifierFlags.shift.rawValue) > 0) {
newFlags |= shiftKey;
}
if ((flags & NSEvent.ModifierFlags.option.rawValue) > 0) {
newFlags |= optionKey
}
if ((flags & NSEvent.ModifierFlags.capsLock.rawValue) > 0) {
newFlags |= alphaLock
}
return UInt32(newFlags);
}
static func register() {
var hotKeyRef: EventHotKeyRef?
let modifierFlags: UInt32 =
getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags.command)
let keyCode = kVK_ANSI_R
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.id = UInt32(keyCode)
// Not sure what "swat" vs "htk1" do.
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
// gMyHotKeyID.signature = OSType("htk1".fourCharCodeValue)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyReleased)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {
(nextHanlder, theEvent, userData) -> OSStatus in
// var hkCom = EventHotKeyID()
// GetEventParameter(theEvent,
// EventParamName(kEventParamDirectObject),
// EventParamType(typeEventHotKeyID),
// nil,
// MemoryLayout<EventHotKeyID>.size,
// nil,
// &hkCom)
NSLog("Command + R Released!")
return noErr
/// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode),
modifierFlags,
gMyHotKeyID,
GetApplicationEventTarget(),
0,
&hotKeyRef)
assert(status == noErr)
}
}
A quick Swift 3 update for the setup:
let opts = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionary
guard AXIsProcessTrustedWithOptions(opts) == true else { return }
NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handler)
I maintain this Swift package that makes it easy to both add global keyboard shortcuts to your app and also let the user set their own.
import SwiftUI
import KeyboardShortcuts
// Declare the shortcut for strongly-typed access.
extension KeyboardShortcuts.Name {
static let toggleUnicornMode = Self("toggleUnicornMode")
}
#main
struct YourApp: App {
#StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
// …
}
Settings {
SettingsScreen()
}
}
}
#MainActor
final class AppState: ObservableObject {
init() {
// Register the listener.
KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) { [self] in
isUnicornMode.toggle()
}
}
}
// Present a view where the user can set the shortcut they want.
struct SettingsScreen: View {
var body: some View {
Form {
HStack(alignment: .firstTextBaseline) {
Text("Toggle Unicorn Mode:")
KeyboardShortcuts.Recorder(for: .toggleUnicornMode)
}
}
}
}
SwiftUI is used in this example, but it also supports Cocoa.
Take a look at the HotKey Library. You can simply use Carthage to implement it into your own app.
HotKey Library
there is a pretty hacky, but also pretty simple workaround if your app has a Menu:
add a new MenuItem (maybe call it something like "Dummy for Hotkey")
in the attributes inspector, conveniently enter your hotkey in the Key Equivalent field
set Allowed when Hidden, Enabled and Hidden to true
link it with an IBAction to do whatever your hotkey is supposed to do
done!