iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: location feed map banner and fix location matching

Add interactive map banner to location feeds (tap to expand). Pass raw
H3 index to match web behavior instead of coarsening to city level,
fixing empty results for venue-level locations. Remove unused h3ToCity.
Also fix story image not filling screen width on iOS 26.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+40 -7
+5 -6
Grain/Utilities/LocationServices.swift
··· 1 + import CoreLocation 1 2 import Foundation 2 3 import SwiftyH3 3 4 ··· 72 73 return cell.description 73 74 } 74 75 75 - /// Coarsen an H3 index to city level (resolution 5). 76 - static func h3ToCity(_ h3Index: String) -> String { 76 + /// Convert an H3 index string to a CLLocationCoordinate2D. 77 + static func h3ToCoordinate(_ h3Index: String) -> CLLocationCoordinate2D? { 77 78 guard let cell = H3Cell(h3Index), 78 - let res = try? cell.resolution, 79 - res.rawValue > 5, 80 - let parent = try? cell.parent(at: .res5) else { return h3Index } 81 - return parent.description 79 + let center = try? cell.center else { return nil } 80 + return center.coordinates 82 81 } 83 82 84 83 /// Reverse geocode coordinates via Nominatim.
+1 -1
Grain/Views/Components/GalleryCardView.swift
··· 178 178 .foregroundStyle(.secondary) 179 179 .lineLimit(1) 180 180 .onTapGesture { 181 - onLocationTap?(LocationServices.h3ToCity(location.value), locationName) 181 + onLocationTap?(location.value, locationName) 182 182 } 183 183 } 184 184 }
+33
Grain/Views/Feed/LocationFeedView.swift
··· 1 + import MapKit 1 2 import SwiftUI 2 3 3 4 struct LocationFeedView: View { ··· 11 12 @State private var selectedHashtag: String? 12 13 @State private var zoomState = ImageZoomState() 13 14 @State private var cardStoryAuthor: GrainStoryAuthor? 15 + @State private var mapInteractive = false 14 16 15 17 let client: XRPCClient 16 18 let h3Index: String ··· 18 20 19 21 private var feedId: String { "location:\(h3Index)" } 20 22 23 + private var coordinate: CLLocationCoordinate2D? { 24 + LocationServices.h3ToCoordinate(h3Index) 25 + } 26 + 21 27 var body: some View { 22 28 ScrollView { 23 29 LazyVStack(spacing: 0) { 30 + if let coord = coordinate { 31 + Map(initialPosition: .region(MKCoordinateRegion( 32 + center: coord, 33 + latitudinalMeters: 20000, 34 + longitudinalMeters: 20000 35 + ))) 36 + .mapStyle(.standard(pointsOfInterest: .excludingAll)) 37 + .mapControlVisibility(mapInteractive ? .automatic : .hidden) 38 + .frame(height: mapInteractive ? 300 : 150) 39 + .overlay { 40 + if !mapInteractive { 41 + Color.clear 42 + .contentShape(Rectangle()) 43 + .onTapGesture { 44 + withAnimation(.easeInOut(duration: 0.25)) { mapInteractive = true } 45 + } 46 + } 47 + } 48 + .mask( 49 + LinearGradient( 50 + colors: mapInteractive ? [.black] : [.black, .black, .clear], 51 + startPoint: .top, 52 + endPoint: .bottom 53 + ) 54 + ) 55 + } 56 + 24 57 ForEach($galleries) { $gallery in 25 58 GalleryCardView(gallery: $gallery, client: client, onNavigate: { 26 59 selectedUri = gallery.uri
+1
Grain/Views/Stories/StoryViewer.swift
··· 147 147 image 148 148 .resizable() 149 149 .aspectRatio(story.aspectRatio.ratio, contentMode: .fit) 150 + .frame(maxWidth: .infinity) 150 151 } else if state.isLoading { 151 152 ProgressView() 152 153 .tint(.white)