diff --git a/RibbonRequest.swift b/RibbonRequest.swift index 256efba..d07202e 100644 --- a/RibbonRequest.swift +++ b/RibbonRequest.swift @@ -15,6 +15,8 @@ import GRDBQuery /// } /// } + +var idColumn = Column("id") struct RibbonRequest: Queryable { // enum Ordering { // case byScore @@ -23,7 +25,7 @@ struct RibbonRequest: Queryable { /// The ordering used by the player request. // var ordering: Ordering - // var book: String + var id: Int64! // MARK: - Queryable Implementation @@ -47,7 +49,11 @@ struct RibbonRequest: Queryable { // This method is not required by Queryable, but it makes it easier func fetchValue(_ db: Database) throws -> [Ribbon] { - return try Ribbon.fetchAll(db) + if (id == nil) { + return try Ribbon.fetchAll(db) + } else { + return try Ribbon.filter(idColumn == id).fetchAll(db) + } // if book == "" { // return try Ribbon.filter(bookColumn == Ribbon.randomBook()).fetchAll(db) diff --git a/SelectedRibbon.swift b/SelectedRibbon.swift new file mode 100644 index 0000000..67e1a65 --- /dev/null +++ b/SelectedRibbon.swift @@ -0,0 +1,51 @@ +// +// Ribbon.swift +// gloss +// +// Created by Saint on 2/24/23. +// + +import GRDB +/// The Line struct. +/// +/// Identifiable conformance supports SwiftUI list animations, and type-safe +/// GRDB primary key methods. +/// Equatable conformance supports tests. +struct SelectedRibbon: Identifiable, Equatable { + /// The player id. + /// + /// Int64 is the recommended type for auto-incremented database ids. + /// Use nil for players that are not inserted yet in the database. + var id: Int64? + var ribbonId: Int64 +} + +extension SelectedRibbon { +} + +// MARK: - Persistence + +/// Make Line a Codable Record. +/// +/// See +extension SelectedRibbon: Codable, FetchableRecord, MutablePersistableRecord { + // Define database columns from CodingKeys + fileprivate enum Columns { + static let id = Column(CodingKeys.id) + static let ribbonId = Column(CodingKeys.ribbonId) + } + + /// Updates a player id after it has been inserted in the database. + mutating func didInsert(_ inserted: InsertionSuccess) { + id = inserted.rowID + } +} + +// MARK: - Line Database Requests + +/// Define some player requests used by the application. +/// +/// See +/// See +extension DerivableRequest { +} diff --git a/SelectedRibbonRequest.swift b/SelectedRibbonRequest.swift new file mode 100644 index 0000000..90a3ce9 --- /dev/null +++ b/SelectedRibbonRequest.swift @@ -0,0 +1,68 @@ +import Combine +import GRDB +import GRDBQuery + +/// A player request can be used with the `@Query` property wrapper in order to +/// feed a view with a list of players. +/// +/// For example: +/// +/// struct MyView: View { +/// @Query(SelectedRibbonRequest(ordering: .byName)) private var players: [SelectedRibbon] +/// +/// var body: some View { +/// List(players) { player in ... ) +/// } +/// } + +struct SelectedRibbonRequest: Queryable { + // enum Ordering { + // case byScore + // case byName + // } + + /// The ordering used by the player request. + // var ordering: Ordering + var id: Int64! + + // MARK: - Queryable Implementation + + static var defaultValue: [SelectedRibbon] { [] } + + func publisher(in appDatabase: AppDatabase) -> AnyPublisher<[SelectedRibbon], Error> { + // Build the publisher from the general-purpose read-only access + // granted by `appDatabase.reader`. + // Some apps will prefer to call a dedicated method of `appDatabase`. + ValueObservation + .tracking(fetchValue(_:)) + .publisher( + in: appDatabase.reader, + // The `.immediate` scheduling feeds the view right on + // subscription, and avoids an undesired animation when the + // application starts. + scheduling: .immediate) + .eraseToAnyPublisher() + } + + // This method is not required by Queryable, but it makes it easier + func fetchValue(_ db: Database) throws -> [SelectedRibbon] { + if (id == nil) { + return try SelectedRibbon.fetchAll(db) + } else { + return try SelectedRibbon.filter(idColumn == id).fetchAll(db) + } + + // if book == "" { + // return try SelectedRibbon.filter(bookColumn == SelectedRibbon.randomBook()).fetchAll(db) + // } else { + // return try SelectedRibbon.filter(bookColumn == book).fetchAll(db) + // } + // switch ordering { + // case .byScore: + // return try SelectedRibbon.all().fetchAll(db) + // case .byName: + // // return try SelectedRibbon.all().orderedByName().fetchAll(db) + // return try SelectedRibbon.all().fetchAll(db) + // } + } +} diff --git a/gloss.xcodeproj/project.pbxproj b/gloss.xcodeproj/project.pbxproj index 35b2bd0..c6917ce 100644 --- a/gloss.xcodeproj/project.pbxproj +++ b/gloss.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 85942EE729AD083600307621 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 85942EE629AD083600307621 /* Introspect */; }; 85942EE929AD51A100307621 /* Ribbon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85942EE829AD51A100307621 /* Ribbon.swift */; }; 85942EEB29AD55A400307621 /* RibbonRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85942EEA29AD55A400307621 /* RibbonRequest.swift */; }; + 85942EED29AEA04200307621 /* SelectedRibbon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85942EEC29AEA04200307621 /* SelectedRibbon.swift */; }; + 85942EEF29AEA18300307621 /* SelectedRibbonRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85942EEE29AEA18300307621 /* SelectedRibbonRequest.swift */; }; 85F01DF82978787800F317B4 /* AveriaSerifLibre-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85F01DF72978787800F317B4 /* AveriaSerifLibre-Regular.ttf */; }; 85F01DFB2978790400F317B4 /* xe-Dogma-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85F01DFA2978790400F317B4 /* xe-Dogma-Bold.ttf */; }; /* End PBXBuildFile section */ @@ -50,6 +52,8 @@ 85942EE329ACF54A00307621 /* ScrollableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableView.swift; sourceTree = ""; }; 85942EE829AD51A100307621 /* Ribbon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ribbon.swift; sourceTree = ""; }; 85942EEA29AD55A400307621 /* RibbonRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RibbonRequest.swift; sourceTree = ""; }; + 85942EEC29AEA04200307621 /* SelectedRibbon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedRibbon.swift; sourceTree = ""; }; + 85942EEE29AEA18300307621 /* SelectedRibbonRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedRibbonRequest.swift; sourceTree = ""; }; 85F01DF72978787800F317B4 /* AveriaSerifLibre-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "AveriaSerifLibre-Regular.ttf"; sourceTree = ""; }; 85F01DFA2978790400F317B4 /* xe-Dogma-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "xe-Dogma-Bold.ttf"; sourceTree = ""; }; 85F01DFC29787B3500F317B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -83,6 +87,8 @@ 85431A7C2905F4F500EE0760 = { isa = PBXGroup; children = ( + 85942EEC29AEA04200307621 /* SelectedRibbon.swift */, + 85942EEE29AEA18300307621 /* SelectedRibbonRequest.swift */, 85942EEA29AD55A400307621 /* RibbonRequest.swift */, 85942EE329ACF54A00307621 /* ScrollableView.swift */, 8590D96A29A8374B001EF84F /* json */, @@ -251,7 +257,9 @@ 8590D96929A18A6D001EF84F /* LineRequest.swift in Sources */, 8590D96E29A927CF001EF84F /* Seg.swift in Sources */, 852774C129A150B100458CA7 /* Line.swift in Sources */, + 85942EEF29AEA18300307621 /* SelectedRibbonRequest.swift in Sources */, 8590D96C29A92146001EF84F /* JsonImport.swift in Sources */, + 85942EED29AEA04200307621 /* SelectedRibbon.swift in Sources */, 85431A892905F4F500EE0760 /* glossApp.swift in Sources */, 85431A9C2905F5D800EE0760 /* SwiftUIView.swift in Sources */, 85942EE429ACF54A00307621 /* ScrollableView.swift in Sources */, diff --git a/gloss.xcodeproj/project.xcworkspace/xcuserdata/saint.xcuserdatad/UserInterfaceState.xcuserstate b/gloss.xcodeproj/project.xcworkspace/xcuserdata/saint.xcuserdatad/UserInterfaceState.xcuserstate index e2ec59e..f6af036 100644 Binary files a/gloss.xcodeproj/project.xcworkspace/xcuserdata/saint.xcuserdatad/UserInterfaceState.xcuserstate and b/gloss.xcodeproj/project.xcworkspace/xcuserdata/saint.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/gloss/AppDatabase.swift b/gloss/AppDatabase.swift index 928aa6a..399f5e9 100644 --- a/gloss/AppDatabase.swift +++ b/gloss/AppDatabase.swift @@ -48,6 +48,11 @@ struct AppDatabase { t.column("book", .text).notNull() t.column("scrollOffset", .integer).notNull() } + + try db.create(table: "SelectedRibbon") { t in + t.autoIncrementedPrimaryKey("id") + t.column("ribbonId", .integer).notNull() + } } @@ -87,6 +92,24 @@ func load(_ filename: String) -> T { extension AppDatabase { + func saveRibbon(_ ribbon: inout Ribbon) async throws { + // if ribbon.name.isEmpty { + // throw ValidationError.missingName + // } + ribbon = try await dbWriter.write { [ribbon] db in + try ribbon.saved(db) + } + } + + func saveSelectedRibbon(_ selectedRibbon: inout SelectedRibbon) async throws { + // if selectedRibbon.name.isEmpty { + // throw ValidationError.missingName + // } + selectedRibbon = try await dbWriter.write { [selectedRibbon] db in + try selectedRibbon.saved(db) + } + } + /// Create random Lines if the database is empty. func createRandomLinesIfEmpty() throws { @@ -103,6 +126,7 @@ extension AppDatabase { // try createRandomLines(db) _ = try Ribbon(id: nil, book: "bible.john", scrollOffset: 0).inserted(db) _ = try Ribbon(id: nil, book: "bible.john", scrollOffset: 2000).inserted(db) + _ = try SelectedRibbon(id: 1, ribbonId: 1).inserted(db) } } } @@ -135,7 +159,7 @@ extension AppDatabase { // MARK: - Database Access: Reads -// This demo app does not provide any specific reading method, and instead +// This demo app does not provide any specific reading method, and insteadKK // gives an unrestricted read-only access to the rest of the application. // In your app, you are free to choose another path, and define focused // reading methods. diff --git a/gloss/ContentView.swift b/gloss/ContentView.swift index f4fc5f9..cbe3134 100644 --- a/gloss/ContentView.swift +++ b/gloss/ContentView.swift @@ -14,6 +14,25 @@ let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "network // var curBook = "John" +extension UserDefaults { + + public func optionalInt(forKey defaultName: String) -> Int? { + let defaults = self + if let value = defaults.value(forKey: defaultName) { + return value as? Int + } + return nil + } + + public func optionalBool(forKey defaultName: String) -> Bool? { + let defaults = self + if let value = defaults.value(forKey: defaultName) { + return value as? Bool + } + return nil + } +} + extension UINavigationController { override open func viewDidLoad() { super.viewDidLoad() @@ -46,23 +65,132 @@ struct BlueButtonStyle: ButtonStyle { } } - -struct SwitchButton : View { - var ribbon: Ribbon - @Binding var selectedRibbon : Ribbon? - @Binding var book : String - @Binding var scrollOffset : CGFloat +struct PositionObservingView: View { + var coordinateSpace: CoordinateSpace +@Binding var position: CGPoint + @ViewBuilder var content: () -> Content var body: some View { - Button("meow") { - book = ribbon.book - selectedRibbon = ribbon - Print(ribbon.scrollOffset) - Print(scrollOffset) - scrollOffset = CGFloat(ribbon.scrollOffset) - //Print("hellosdf dddd") + content() + .background(GeometryReader { geometry in + Color.clear.preference( + key: PreferenceKey.self, + value: geometry.frame(in: coordinateSpace).origin +) + }) + .onPreferenceChange(PreferenceKey.self) { position in + self.position = position + // Print(self.position) + } + } +} + + +private extension PositionObservingView { + struct PreferenceKey: SwiftUI.PreferenceKey { + static var defaultValue: CGPoint { .zero } + + static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { + // No-op } - .buttonStyle(BlueButtonStyle()) + } +} + +struct OffsetObservingScrollView: View { + var axes: Axis.Set = [.vertical] + var showsIndicators = true + @Binding var offset: CGPoint + @ViewBuilder var content: () -> Content + + // The name of our coordinate space doesn't have to be + // stable between view updates (it just needs to be + // consistent within this view), so we'll simply use a + // plain UUID for it: + private let coordinateSpaceName = UUID() + + var body: some View { + ScrollView(axes, showsIndicators: showsIndicators) { + PositionObservingView( + coordinateSpace: .named(coordinateSpaceName), + position: Binding( + get: { offset }, + set: { newOffset in + offset = CGPoint( + x: -newOffset.x, + y: -newOffset.y +) + } + ), + content: content + ) + } + .coordinateSpace(name: coordinateSpaceName) + } +} + + +struct SwitchButton : View { + @State var ribbon: Ribbon + // @State var selectedRibbon: SelectedRibbon + @Binding var book : String + @Binding var scrollOffset : CGFloat + @Binding var readOffset: CGPoint + + @Environment(\.appDatabase) private var appDatabase + + + @Query(SelectedRibbonRequest()) private var sr: [SelectedRibbon] + + @State var saveOffset = CGFloat() + var body: some View { + // ForEach(sr) { selectedRibbon in + Button("meow", + action: { + Task { + var selectedRibbon = sr[0] + + saveOffset = CGFloat(readOffset.y) + Print("save/read offset", readOffset.y) + + Print("START") + _ = Print(ribbon) + _ = Print(selectedRibbon) + Print("START2") + + if (selectedRibbon.ribbonId != ribbon.id!) { + Print(ribbon.id!) + Print("switching") + book = ribbon.book + scrollOffset = CGFloat(ribbon.scrollOffset) + // scrollOffset = CGFloat(1500) + Print("setted offset value", scrollOffset) + selectedRibbon.ribbonId = ribbon.id! + _ = try await appDatabase.saveSelectedRibbon(&selectedRibbon) + _ = Print(selectedRibbon) + } + var editedRibbon = ribbon + editedRibbon.scrollOffset = Int(saveOffset) + Print("current ribbon offset after potential switch", editedRibbon.scrollOffset) + + _ = try await appDatabase.saveRibbon(&editedRibbon) + _ = Print(editedRibbon) + + Print("ribbon offset saved") + + Print("set offset value", scrollOffset) + Print("read offset value", readOffset.y) + + + Print("END") + _ = Print(ribbon) + _ = Print(selectedRibbon) + Print("END2") + } + } + + ) + .buttonStyle(BlueButtonStyle()) + // } } } @@ -75,32 +203,47 @@ struct ContentView: View { // @State var scrollTo1 : Int64? @State var selectedLine : Int64? @State var scrollOffset = CGFloat() - @State var selectedRibbon: Ribbon! + + // @State var selectedRibbon: Ribbon!, + // @State var selectedRibbon: Ribbon! + + // @State var selectedRibbonId = Int64(UserDefaults.standard.optionalInt(forKey: "selectedRibbonId") ?? 1) + + + @Query(SelectedRibbonRequest()) private var selectedRibbon: [SelectedRibbon] + + @State var endedDrag = true + + @State var readOffset = CGPoint() @Query(LineRequest(ordering: .byScore, book: "bible.john")) private var lines: [Line] + @Query(RibbonRequest()) private var ribbons: [Ribbon] - // @Query private var lines: [Line] + // @Query*RibbonRequest> private var selectedRibbon: [Ribbon] + + // @Query(RibbonRequest(id: Int64(UserDefaults.standard.optionalInt(forKey: "lastRibbonId") ?? 1))) private var selectedRibbon: [Ribbon] init() { UITableView.appearance().backgroundColor = UIColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - // _lines = Query(LineRequest(ordering: .byScore, book: curBook)) + // _selectedRibbon = Query(RibbonRequest(id: Int64(UserDefaults.standard.optionalInt(forKey: "lastRibbonId") ?? 1))) } - // @Environment(\.managedObjectContext) private var viewContext - - var body: some View { GeometryReader { geometry in ZStack{ VStack{ - ForEach(ribbons) { ribbon in - SwitchButton(ribbon: ribbon, - selectedRibbon: $selectedRibbon, - book: $lines.book, - scrollOffset: $scrollOffset) - .buttonStyle(BlueButtonStyle()) + ForEach(ribbons) { ribbon in + ForEach(selectedRibbon) { sr in + SwitchButton(ribbon: ribbon, + // selectedRibbon:sr, + book: $lines.book, + scrollOffset: $scrollOffset, + readOffset: $readOffset + ) + .buttonStyle(BlueButtonStyle()) + } } } .frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading) @@ -109,7 +252,8 @@ struct ContentView: View { // VStack (spacing: 0){ ScrollViewReader { proxy in //ScrollableView($offset, animationDuration: 0) { - ScrollView { + OffsetObservingScrollView(offset: $readOffset) + { // List { LazyVStack { ForEach(lines) { line in @@ -143,7 +287,10 @@ struct ContentView: View { .introspectScrollView { scrollView in //let width = scrollView.contentSize.width - scrollView.frame.width //Print(scrollView.contentOffset.x) - scrollView.contentOffset.y = scrollOffset + // Print("here") + if (scrollOffset != scrollView.contentOffset.y) { + scrollView.contentOffset.y = scrollOffset + } } .listStyle(PlainListStyle()) @@ -154,15 +301,15 @@ struct ContentView: View { .offset(x:30 , y:0) .offset(x: pulledOut.width) .offset(x: viewState.width, y: viewState.height) - .onAppear { - if (selectedRibbon != nil) { - Print(selectedRibbon) - //scrollOffset.y = scrollOffset - } - } .gesture( DragGesture() .onChanged { gesture in + + if (endedDrag) { + endedDrag = false + scrollOffset = readOffset.y - 20 + _ = Print("meow") + } // logger.error("hello222") // NSLog("hellooo") //Print(viewState.width) @@ -171,6 +318,7 @@ struct ContentView: View { // logger.log("hello") } .onEnded { _ in + endedDrag = true var pulledOutWidth = CGFloat(0) if (viewState.width < 0) { pulledOutWidth = CGFloat(0) diff --git a/gloss/Ribbon.swift b/gloss/Ribbon.swift index 5ad0fba..cabd501 100644 --- a/gloss/Ribbon.swift +++ b/gloss/Ribbon.swift @@ -49,44 +49,5 @@ extension Ribbon: Codable, FetchableRecord, MutablePersistableRecord { /// /// See /// See -extension DerivableRequest { - /// A request of players ordered by name. - /// - /// For example: - /// - /// let players: [Line] = try dbWriter.read { db in - /// try Line.all().orderedByName().fetchAll(db) - /// } - //func orderedByName() -> Self { - // // Sort by name in a localized case insensitive fashion - // // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison - // order(Line.Columns.name.collating(.localizedCaseInsensitiveCompare)) - //} - - ///// A request of players ordered by score. - ///// - ///// For example: - ///// - ///// let players: [Line] = try dbWriter.read { db in - ///// try Line.all().orderedByScore().fetchAll(db) - ///// } - ///// let bestLine: Line? = try dbWriter.read { db in - ///// try Line.all().orderedByScore().fetchOne(db) - ///// } - //func orderedByScore() -> Self { - // // Sort by descending score, and then by name, in a - // // localized case insensitive fashion - // // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison - // order( - // Line.Columns.score.desc, - // Line.Columns.name.collating(.localizedCaseInsensitiveCompare)) - //} +extension DerivableRequest { } - -// Ribbon.swift -// gloss -// -// Created by Saint on 2/27/23. -// - -import Foundation