v15: Widget, Mitteilungs-Knoepfe, Akku, neues Design
This commit is contained in:
1 parent
98a94e765e
commit
425fa3cbd3
30 files changed
+1651
-655
No files matched your search
@@ -63,14 +63,14 @@
|
||||
|
||||
// grosse Weckzeit links, darunter die Fahrt
|
||||
NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:[BVGAPI hhmm:n.fireDate]
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme digits:26 weight:UIFontWeightSemibold],
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme digits:22 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: [BVGTheme textPrimary] }];
|
||||
NSInteger mins = (NSInteger)floor([n.fireDate timeIntervalSinceNow] / 60.0);
|
||||
NSString *rest = mins >= 60 ? [NSString stringWithFormat:@" in %ld:%02ld h", (long)(mins / 60), (long)(mins % 60)]
|
||||
: [NSString stringWithFormat:@" in %ld min", (long)MAX(mins, 0)];
|
||||
[title appendAttributedString:[[NSAttributedString alloc] initWithString:rest
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:14 weight:UIFontWeightMedium],
|
||||
NSForegroundColorAttributeName: [BVGTheme orange] }]];
|
||||
NSForegroundColorAttributeName: [BVGTheme warningText] }]];
|
||||
cell.textLabel.attributedText = title;
|
||||
|
||||
NSMutableString *sub = [NSMutableString string];
|
||||
|
||||
+66
-15
@@ -2,8 +2,7 @@
|
||||
#import "ViewController.h"
|
||||
#import "MapViewController.h"
|
||||
#import "JourneyViewController.h"
|
||||
#import "SearchViewController.h"
|
||||
#import "AlarmsViewController.h"
|
||||
#import "SettingsViewController.h"
|
||||
#import "StopViewController.h"
|
||||
#import "GuidanceManager.h"
|
||||
#import "GuidanceViewController.h"
|
||||
@@ -48,18 +47,35 @@ static void BVGUncaughtException(NSException *e) {
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
NSSetUncaughtExceptionHandler(&BVGUncaughtException);
|
||||
// Knoepfe direkt in der Mitteilung (Sperrbildschirm: nach links wischen, Banner: herunterziehen)
|
||||
UIMutableUserNotificationAction *accept = [[UIMutableUserNotificationAction alloc] init];
|
||||
accept.identifier = @"accept";
|
||||
accept.title = @"Übernehmen";
|
||||
accept.activationMode = UIUserNotificationActivationModeBackground;
|
||||
accept.authenticationRequired = NO;
|
||||
UIMutableUserNotificationAction *keep = [[UIMutableUserNotificationAction alloc] init];
|
||||
keep.identifier = @"keep";
|
||||
keep.title = @"Behalten";
|
||||
keep.activationMode = UIUserNotificationActivationModeBackground;
|
||||
keep.authenticationRequired = NO;
|
||||
UIMutableUserNotificationCategory *altCategory = [[UIMutableUserNotificationCategory alloc] init];
|
||||
altCategory.identifier = GuidanceAlternativeCategory;
|
||||
[altCategory setActions:@[accept, keep] forContext:UIUserNotificationActionContextDefault];
|
||||
[altCategory setActions:@[accept, keep] forContext:UIUserNotificationActionContextMinimal];
|
||||
|
||||
[application registerUserNotificationSettings:
|
||||
[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge)
|
||||
categories:nil]];
|
||||
categories:[NSSet setWithObject:altCategory]]];
|
||||
[BVGAPI syncWidget]; // Favoriten fuers Widget bereitstellen
|
||||
|
||||
[BVGTheme applyAppearance]; // modernes Design (Farben, Leisten, Schriften)
|
||||
|
||||
// 4 Tabs: Suche steckt in "Abfahrten", Wecker & Einstellungen in "Mehr"
|
||||
NSArray *roots = @[
|
||||
@[[[ViewController alloc] initWithStyle:UITableViewStylePlain], @"In der Nähe", @"nearby"],
|
||||
@[[[MapViewController alloc] init], @"Karte", @"map"],
|
||||
@[[[JourneyViewController alloc] initWithStyle:UITableViewStyleGrouped], @"Route", @"route"],
|
||||
@[[[SearchViewController alloc] initWithStyle:UITableViewStylePlain], @"Suche", @"search"],
|
||||
@[[[AlarmsViewController alloc] initWithStyle:UITableViewStylePlain], @"Wecker", @"alarm"],
|
||||
@[[[ViewController alloc] initWithStyle:UITableViewStylePlain], @"Abfahrten", @"nearby"],
|
||||
@[[[JourneyViewController alloc] initWithStyle:UITableViewStyleGrouped], @"Verbindung", @"route"],
|
||||
@[[[MapViewController alloc] init], @"Karte", @"map"],
|
||||
@[[[SettingsViewController alloc] initWithStyle:UITableViewStyleGrouped], @"Mehr", @"more"],
|
||||
];
|
||||
NSMutableArray *navs = [NSMutableArray array];
|
||||
NSInteger tag = 0;
|
||||
@@ -71,6 +87,7 @@ static void BVGUncaughtException(NSException *e) {
|
||||
|
||||
self.tabs = [[UITabBarController alloc] init];
|
||||
self.tabs.viewControllers = navs;
|
||||
[BVGTheme applyTabBarIndicator:self.tabs.tabBar count:navs.count];
|
||||
|
||||
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
self.window.backgroundColor = [BVGTheme background];
|
||||
@@ -99,16 +116,16 @@ static void BVGUncaughtException(NSException *e) {
|
||||
}
|
||||
|
||||
- (void)updateGuidanceBadge {
|
||||
UIViewController *routeTab = self.tabs.viewControllers[2];
|
||||
UIViewController *routeTab = self.tabs.viewControllers[1];
|
||||
GuidanceManager *g = [GuidanceManager shared];
|
||||
if (!g.active) routeTab.tabBarItem.badgeValue = nil;
|
||||
else routeTab.tabBarItem.badgeValue = (g.status == GuidanceStatusBroken || g.alternative) ? @"!" : @"▶";
|
||||
else routeTab.tabBarItem.badgeValue = (g.status == GuidanceStatusBroken || g.alternative) ? @"!" : @""; // leer = kleiner Punkt
|
||||
}
|
||||
|
||||
- (void)openGuidance {
|
||||
if (![GuidanceManager shared].active) return;
|
||||
[self.tabs dismissViewControllerAnimated:NO completion:nil];
|
||||
self.tabs.selectedIndex = 2;
|
||||
self.tabs.selectedIndex = 1;
|
||||
UINavigationController *nav = (UINavigationController *)self.tabs.selectedViewController;
|
||||
if ([nav.topViewController isKindOfClass:[GuidanceViewController class]]) return;
|
||||
[nav popToRootViewControllerAnimated:NO];
|
||||
@@ -119,7 +136,7 @@ static void BVGUncaughtException(NSException *e) {
|
||||
for (NSString *icon in @[@"🚏 ", @"🔔 ", @"⚠ ", @"→"]) {
|
||||
text = [text stringByReplacingOccurrencesOfString:icon withString:([icon isEqualToString:@"→"] ? @"Richtung" : @"")];
|
||||
}
|
||||
return [text stringByReplacingOccurrencesOfString:@"Antippen zum Entscheiden." withString:@""];
|
||||
return [text stringByReplacingOccurrencesOfString:@"Wischen oder antippen zum Entscheiden." withString:@""];
|
||||
}
|
||||
|
||||
- (void)updateAlarmBadge {
|
||||
@@ -136,22 +153,56 @@ static void BVGUncaughtException(NSException *e) {
|
||||
#pragma mark - Spotlight: Haltestelle aus der iOS-Suche oeffnen
|
||||
|
||||
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity
|
||||
restorationHandler:(void (^)(NSArray *))restorationHandler {
|
||||
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
|
||||
if (![userActivity.activityType isEqualToString:CSSearchableItemActionType]) return NO;
|
||||
NSString *stopId = [BVGAPI stopIdFromSpotlightIdentifier:userActivity.userInfo[CSSearchableItemActivityIdentifier]];
|
||||
if (!stopId) return NO;
|
||||
[self openStop:stopId name:nil];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)openStop:(NSString *)stopId name:(NSString *)name {
|
||||
[self.tabs dismissViewControllerAnimated:NO completion:nil];
|
||||
self.tabs.selectedIndex = 0;
|
||||
UINavigationController *nav = (UINavigationController *)self.tabs.selectedViewController;
|
||||
[nav popToRootViewControllerAnimated:NO];
|
||||
|
||||
StopViewController *vc = [[StopViewController alloc] initWithStopId:stopId
|
||||
name:[BVGAPI nameForStop:stopId] ?: @"Haltestelle"];
|
||||
name:name ?: [BVGAPI nameForStop:stopId] ?: @"Haltestelle"];
|
||||
[nav pushViewController:vc animated:NO];
|
||||
}
|
||||
|
||||
#pragma mark - Widget: bvgnearby://stop?id=...&name=...
|
||||
|
||||
- (BOOL)handleURL:(NSURL *)url {
|
||||
if (![url.scheme isEqualToString:@"bvgnearby"]) return NO;
|
||||
NSMutableDictionary *q = [NSMutableDictionary dictionary];
|
||||
for (NSString *pair in [url.query componentsSeparatedByString:@"&"]) {
|
||||
NSArray *kv = [pair componentsSeparatedByString:@"="];
|
||||
if (kv.count == 2) q[kv[0]] = [kv[1] stringByRemovingPercentEncoding] ?: kv[1];
|
||||
}
|
||||
if ([url.host isEqualToString:@"stop"] && q[@"id"]) [self openStop:q[@"id"] name:q[@"name"]];
|
||||
else self.tabs.selectedIndex = 0; // bvgnearby:// -> Abfahrten
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *, id> *)options {
|
||||
return [self handleURL:url];
|
||||
}
|
||||
|
||||
#pragma mark - Knoepfe in der Alternativ-Mitteilung
|
||||
|
||||
- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier
|
||||
forLocalNotification:(UILocalNotification *)notification completionHandler:(void (^)(void))completionHandler {
|
||||
GuidanceManager *g = [GuidanceManager shared];
|
||||
if ([identifier isEqualToString:@"accept"]) [g acceptAlternative];
|
||||
else if ([identifier isEqualToString:@"keep"]) [g dismissAlternative];
|
||||
// kurz Zeit lassen, damit neue Erinnerungen geplant und gespeichert werden
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
if (completionHandler) completionHandler();
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Wecker
|
||||
|
||||
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
|
||||
|
||||
+7
-1
@@ -26,8 +26,13 @@ extern NSString * const BVGAlarmsChangedNotification;
|
||||
+ (UIImage *)badgeForLine:(NSDictionary *)line;
|
||||
+ (UIImage *)badgeStripForLines:(NSArray *)lines;
|
||||
+ (UIImage *)dotWithColor:(UIColor *)color filled:(BOOL)filled;
|
||||
+ (UIImage *)tabIconNamed:(NSString *)name;
|
||||
+ (UIImage *)tabIconNamed:(NSString *)name; // nearby, search, map, route, alarm, more
|
||||
+ (UIImage *)smallIconNamed:(NSString *)name size:(CGFloat)size; // Tab-Icon verkleinert
|
||||
+ (UIImage *)stopSignWithSize:(CGFloat)diameter; // gelber Kreis mit gruenem H
|
||||
+ (NSString *)spokenLineName:(NSDictionary *)line;
|
||||
+ (NSString *)shortName:(NSString *)name; // "U Amrumer Str. (Berlin)" -> "U Amrumer Str."
|
||||
+ (NSString *)platform:(NSString *)platform; // "2 (U7)" -> "2"
|
||||
+ (UIBarButtonItem *)barButtonWithIcon:(NSString *)icon target:(id)target action:(SEL)action; // speaker, locate
|
||||
|
||||
// Favoriten & zuletzt angesehen
|
||||
+ (NSArray *)favorites;
|
||||
@@ -36,6 +41,7 @@ extern NSString * const BVGAlarmsChangedNotification;
|
||||
+ (NSArray *)recentStops;
|
||||
+ (void)addRecentStop:(NSString *)stopId name:(NSString *)name;
|
||||
+ (NSString *)nameForStop:(NSString *)stopId;
|
||||
+ (void)syncWidget; // Favoriten fuer das Mitteilungszentrale-Widget bereitstellen (App Group)
|
||||
|
||||
// Spotlight (iOS 9 Suche / Siri-Vorschlaege)
|
||||
+ (void)indexStopInSpotlight:(NSString *)stopId name:(NSString *)name;
|
||||
|
||||
+102
-4
@@ -10,6 +10,7 @@ static NSString *gLastSource = nil;
|
||||
static NSString * const kFavoritesKey = @"favorites";
|
||||
static NSString * const kRecentsKey = @"recentStops";
|
||||
static NSString * const kSpotlightPrefix = @"stop:";
|
||||
static NSString * const kAppGroup = @"group.com.serverlele.BVGNearby"; // gemeinsam mit dem Widget
|
||||
|
||||
NSString * const BVGFavoritesChangedNotification = @"BVGFavoritesChanged";
|
||||
NSString * const BVGAlarmsChangedNotification = @"BVGAlarmsChanged";
|
||||
@@ -192,13 +193,15 @@ NSString * const BVGAlarmsChangedNotification = @"BVGAlarmsChanged";
|
||||
UIImage *cached = [cache objectForKey:key];
|
||||
if (cached) return cached;
|
||||
|
||||
NSDictionary *attrs = @{ NSFontAttributeName: [UIFont systemFontOfSize:14 weight:UIFontWeightBold],
|
||||
NSDictionary *attrs = @{ NSFontAttributeName: [UIFont systemFontOfSize:13 weight:UIFontWeightBold],
|
||||
NSForegroundColorAttributeName: fg ?: [UIColor whiteColor] };
|
||||
CGSize textSize = [name sizeWithAttributes:attrs];
|
||||
CGSize size = CGSizeMake(MAX(42, ceil(textSize.width) + 16), 24);
|
||||
CGSize size = CGSizeMake(MAX(38, ceil(textSize.width) + 12), 22);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
||||
CGFloat radius = [product isEqualToString:@"suburban"] ? size.height / 2 : 7;
|
||||
// S-Bahn rund, U-Bahn fast eckig, Rest leicht abgerundet - wie auf den Anzeigen
|
||||
CGFloat radius = [product isEqualToString:@"suburban"] ? size.height / 2
|
||||
: ([product isEqualToString:@"subway"] ? 3 : 5);
|
||||
[[self colorForLine:line] setFill];
|
||||
[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, size.width, size.height) cornerRadius:radius] fill];
|
||||
[name drawAtPoint:CGPointMake((size.width - textSize.width) / 2, (size.height - textSize.height) / 2)
|
||||
@@ -221,7 +224,7 @@ NSString * const BVGAlarmsChangedNotification = @"BVGAlarmsChanged";
|
||||
}
|
||||
if (!imgs.count) return nil;
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(width - 4, 24), NO, 0);
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(width - 4, 22), NO, 0);
|
||||
CGFloat x = 0;
|
||||
for (UIImage *b in imgs) {
|
||||
[b drawAtPoint:CGPointMake(x, 0)];
|
||||
@@ -232,6 +235,24 @@ NSString * const BVGAlarmsChangedNotification = @"BVGAlarmsChanged";
|
||||
return img;
|
||||
}
|
||||
|
||||
+ (NSString *)shortName:(NSString *)name {
|
||||
if (![name isKindOfClass:[NSString class]] || !name.length) return name;
|
||||
return [name stringByReplacingOccurrencesOfString:@"\\s*\\((Berlin|Bln)\\)" withString:@""
|
||||
options:NSRegularExpressionSearch range:NSMakeRange(0, name.length)];
|
||||
}
|
||||
|
||||
+ (NSString *)platform:(NSString *)platform {
|
||||
if (![platform isKindOfClass:[NSString class]] || !platform.length) return nil;
|
||||
NSString *p = [platform stringByReplacingOccurrencesOfString:@"\\s*\\(.*\\)" withString:@""
|
||||
options:NSRegularExpressionSearch range:NSMakeRange(0, platform.length)];
|
||||
return p.length ? p : platform;
|
||||
}
|
||||
|
||||
+ (UIBarButtonItem *)barButtonWithIcon:(NSString *)icon target:(id)target action:(SEL)action {
|
||||
return [[UIBarButtonItem alloc] initWithImage:[self smallIconNamed:icon size:22]
|
||||
style:UIBarButtonItemStylePlain target:target action:action];
|
||||
}
|
||||
|
||||
+ (NSString *)spokenLineName:(NSDictionary *)line {
|
||||
NSString *name = NN(line[@"name"]) ?: @"";
|
||||
NSString *product = NN(line[@"product"]) ?: @"";
|
||||
@@ -253,6 +274,41 @@ NSString * const BVGAlarmsChangedNotification = @"BVGAlarmsChanged";
|
||||
return img;
|
||||
}
|
||||
|
||||
+ (UIImage *)smallIconNamed:(NSString *)name size:(CGFloat)size {
|
||||
UIImage *big = [self tabIconNamed:name];
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(size, size), NO, 0);
|
||||
[big drawInRect:CGRectMake(0, 0, size, size)];
|
||||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return [img imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
}
|
||||
|
||||
// Haltestellenschild: gelber Kreis mit gruenem "H" (wie an jeder deutschen Haltestelle)
|
||||
+ (UIImage *)stopSignWithSize:(CGFloat)d {
|
||||
static NSCache *cache;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{ cache = [[NSCache alloc] init]; });
|
||||
NSNumber *key = @(d);
|
||||
UIImage *img = [cache objectForKey:key];
|
||||
if (img) return img;
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(d, d), NO, 0);
|
||||
UIColor *green = [UIColor colorWithRed:0 green:0.46 blue:0.25 alpha:1];
|
||||
[green setFill];
|
||||
[[UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, d, d)] fill];
|
||||
[[UIColor colorWithRed:0.94 green:0.84 blue:0.13 alpha:1] setFill];
|
||||
CGFloat ring = MAX(1.5, d * 0.07);
|
||||
[[UIBezierPath bezierPathWithOvalInRect:CGRectMake(ring, ring, d - 2 * ring, d - 2 * ring)] fill];
|
||||
NSDictionary *attrs = @{ NSFontAttributeName: [UIFont systemFontOfSize:d * 0.6 weight:UIFontWeightHeavy],
|
||||
NSForegroundColorAttributeName: green };
|
||||
CGSize t = [@"H" sizeWithAttributes:attrs];
|
||||
[@"H" drawAtPoint:CGPointMake((d - t.width) / 2, (d - t.height) / 2) withAttributes:attrs];
|
||||
img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
[cache setObject:img forKey:key];
|
||||
return img;
|
||||
}
|
||||
|
||||
+ (UIImage *)tabIconNamed:(NSString *)name {
|
||||
CGSize size = CGSizeMake(25, 25);
|
||||
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
||||
@@ -304,6 +360,40 @@ NSString * const BVGAlarmsChangedNotification = @"BVGAlarmsChanged";
|
||||
CGFloat dash[] = {3, 3};
|
||||
[path setLineDash:dash count:2 phase:0];
|
||||
[path stroke];
|
||||
} else if ([name isEqualToString:@"speaker"]) {
|
||||
// Lautsprecher mit zwei Schallboegen
|
||||
UIBezierPath *body = [UIBezierPath bezierPath];
|
||||
[body moveToPoint:CGPointMake(3, 9)];
|
||||
[body addLineToPoint:CGPointMake(7, 9)];
|
||||
[body addLineToPoint:CGPointMake(12, 4)];
|
||||
[body addLineToPoint:CGPointMake(12, 21)];
|
||||
[body addLineToPoint:CGPointMake(7, 16)];
|
||||
[body addLineToPoint:CGPointMake(3, 16)];
|
||||
[body closePath];
|
||||
[body fill];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
CGFloat r = 5 + i * 4.5;
|
||||
UIBezierPath *wave = [UIBezierPath bezierPathWithArcCenter:CGPointMake(12, 12.5) radius:r
|
||||
startAngle:-M_PI / 3.5 endAngle:M_PI / 3.5 clockwise:YES];
|
||||
wave.lineWidth = 2;
|
||||
wave.lineCapStyle = kCGLineCapRound;
|
||||
[wave stroke];
|
||||
}
|
||||
} else if ([name isEqualToString:@"locate"]) {
|
||||
// Ortungspfeil (wie in Apple Karten)
|
||||
UIBezierPath *arrow = [UIBezierPath bezierPath];
|
||||
[arrow moveToPoint:CGPointMake(22, 3)];
|
||||
[arrow addLineToPoint:CGPointMake(3, 11.5)];
|
||||
[arrow addLineToPoint:CGPointMake(12, 13)];
|
||||
[arrow addLineToPoint:CGPointMake(13.5, 22)];
|
||||
[arrow closePath];
|
||||
arrow.lineWidth = 2;
|
||||
arrow.lineJoinStyle = kCGLineJoinRound;
|
||||
[arrow stroke];
|
||||
} else if ([name isEqualToString:@"more"]) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
[[UIBezierPath bezierPathWithOvalInRect:CGRectMake(3 + i * 7.5, 10, 4.5, 4.5)] fill];
|
||||
}
|
||||
} else if ([name isEqualToString:@"alarm"]) {
|
||||
UIBezierPath *face = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(3, 4, 19, 19)];
|
||||
face.lineWidth = 2;
|
||||
@@ -342,9 +432,17 @@ NSString * const BVGAlarmsChangedNotification = @"BVGAlarmsChanged";
|
||||
[[NSUserDefaults standardUserDefaults] setObject:list forKey:kFavoritesKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
if (fav) [self indexStopInSpotlight:stopId name:name];
|
||||
[self syncWidget];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:BVGFavoritesChangedNotification object:nil];
|
||||
}
|
||||
|
||||
+ (void)syncWidget {
|
||||
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:kAppGroup];
|
||||
[shared setObject:[self favorites] forKey:@"favorites"];
|
||||
[shared setBool:[[NSUserDefaults standardUserDefaults] boolForKey:@"set.ownServer"] forKey:@"ownServer"];
|
||||
[shared synchronize];
|
||||
}
|
||||
|
||||
#pragma mark - Zuletzt angesehen
|
||||
|
||||
+ (NSArray *)recentStops {
|
||||
|
||||
+20
-11
@@ -1,31 +1,40 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Zentrales Design: Farben, Schriften, Systemdarstellung.
|
||||
// Orientiert an aktuellem iOS-Design (inset grouped, grosse Titel), umgesetzt mit iOS-9-Mitteln.
|
||||
// Zentrales Design im Stil der BVG-App: gelber Kopfbereich, kraeftige schwarze Schrift,
|
||||
// weisse Karten auf hellgrauem Grund. Nur iOS-9-Mittel.
|
||||
@interface BVGTheme : NSObject
|
||||
|
||||
+ (void)applyAppearance;
|
||||
+ (void)applyTabBarIndicator:(UITabBar *)bar count:(NSUInteger)count; // gelber Balken ueber dem aktiven Tab
|
||||
|
||||
// Farben
|
||||
+ (UIColor *)background; // hellgrauer Hintergrund (#F2F2F7)
|
||||
+ (UIColor *)card; // Kartenflaeche (weiss)
|
||||
+ (UIColor *)cardPressed; // Karte beim Antippen
|
||||
+ (UIColor *)background; // hellgrauer Hintergrund
|
||||
+ (UIColor *)card; // Kartenflaeche (weiss)
|
||||
+ (UIColor *)cardPressed; // Karte beim Antippen
|
||||
+ (UIColor *)separator;
|
||||
+ (UIColor *)textPrimary;
|
||||
+ (UIColor *)textPrimary; // schwarz
|
||||
+ (UIColor *)textSecondary;
|
||||
+ (UIColor *)textTertiary;
|
||||
+ (UIColor *)accent; // BVG-Gelb
|
||||
+ (UIColor *)accentSoft; // helles Gelb fuer Hervorhebungen
|
||||
+ (UIColor *)link; // iOS-Blau
|
||||
+ (UIColor *)accent; // BVG-Gelb
|
||||
+ (UIColor *)accentSoft; // helles Gelb fuer Hervorhebungen
|
||||
+ (UIColor *)header; // Kopfbereich (= Gelb)
|
||||
+ (UIColor *)headerText; // Schrift auf Gelb
|
||||
+ (UIColor *)headerSecondary; // Nebentext auf Gelb (dunkel, gut lesbar)
|
||||
+ (UIColor *)warningText; // dunkles Orange, lesbar auf Gelb und Weiss
|
||||
+ (UIColor *)searchField; // Suchfeld-Hintergrund
|
||||
+ (UIColor *)link;
|
||||
+ (UIColor *)green;
|
||||
+ (UIColor *)red;
|
||||
+ (UIColor *)orange;
|
||||
|
||||
// Schriften (San Francisco)
|
||||
+ (UIFont *)font:(CGFloat)size weight:(CGFloat)weight;
|
||||
+ (UIFont *)digits:(CGFloat)size weight:(CGFloat)weight; // gleich breite Ziffern -> kein Springen beim Herunterzaehlen
|
||||
+ (UIFont *)digits:(CGFloat)size weight:(CGFloat)weight; // gleich breite Ziffern -> kein Springen
|
||||
|
||||
+ (CGFloat)cardInset; // Abstand Karte <-> Bildschirmrand
|
||||
+ (CGFloat)cardInset; // Abstand Karte <-> Bildschirmrand
|
||||
+ (CGFloat)cardRadius;
|
||||
|
||||
+ (UIImage *)imageWithColor:(UIColor *)color;
|
||||
+ (UIImage *)roundedImageWithColor:(UIColor *)color radius:(CGFloat)radius; // dehnbar, z.B. Suchfeld
|
||||
|
||||
@end
|
||||
+50
-22
@@ -6,19 +6,24 @@
|
||||
return [UIColor colorWithRed:r / 255.0 green:g / 255.0 blue:b / 255.0 alpha:1];
|
||||
}
|
||||
|
||||
+ (UIColor *)background { return [self rgb:242 :242 :247]; }
|
||||
+ (UIColor *)card { return [UIColor whiteColor]; }
|
||||
+ (UIColor *)cardPressed { return [self rgb:229 :229 :234]; }
|
||||
+ (UIColor *)separator { return [self rgb:224 :224 :228]; }
|
||||
+ (UIColor *)textPrimary { return [self rgb:17 :17 :20]; }
|
||||
+ (UIColor *)textSecondary { return [self rgb:120 :120 :128]; }
|
||||
+ (UIColor *)textTertiary { return [self rgb:174 :174 :178]; }
|
||||
+ (UIColor *)accent { return [self rgb:247 :209 :23]; }
|
||||
+ (UIColor *)accentSoft { return [self rgb:255 :246 :204]; }
|
||||
+ (UIColor *)link { return [self rgb:0 :122 :255]; }
|
||||
+ (UIColor *)green { return [self rgb:52 :168 :83]; }
|
||||
+ (UIColor *)red { return [self rgb:255 :59 :48]; }
|
||||
+ (UIColor *)orange { return [self rgb:255 :149 :0]; }
|
||||
+ (UIColor *)background { return [self rgb:242 :242 :245]; }
|
||||
+ (UIColor *)card { return [UIColor whiteColor]; }
|
||||
+ (UIColor *)cardPressed { return [self rgb:228 :228 :228]; }
|
||||
+ (UIColor *)separator { return [self rgb:226 :226 :226]; }
|
||||
+ (UIColor *)textPrimary { return [self rgb:28 :28 :30]; }
|
||||
+ (UIColor *)textSecondary { return [self rgb:118 :118 :124]; }
|
||||
+ (UIColor *)textTertiary { return [self rgb:165 :165 :165]; }
|
||||
+ (UIColor *)accent { return [self rgb:240 :215 :34]; }
|
||||
+ (UIColor *)accentSoft { return [self rgb:255 :245 :190]; }
|
||||
+ (UIColor *)header { return [self accent]; }
|
||||
+ (UIColor *)headerText { return [self rgb:0 :0 :0]; }
|
||||
+ (UIColor *)headerSecondary { return [self rgb:60 :54 :20]; }
|
||||
+ (UIColor *)warningText { return [self rgb:156 :72 :0]; }
|
||||
+ (UIColor *)searchField { return [UIColor whiteColor]; }
|
||||
+ (UIColor *)link { return [self rgb:0 :94 :184]; }
|
||||
+ (UIColor *)green { return [self rgb:0 :140 :60]; }
|
||||
+ (UIColor *)red { return [self rgb:210 :20 :30]; }
|
||||
+ (UIColor *)orange { return [self rgb:235 :120 :0]; }
|
||||
|
||||
+ (UIFont *)font:(CGFloat)size weight:(CGFloat)weight {
|
||||
return [UIFont systemFontOfSize:size weight:weight];
|
||||
@@ -28,8 +33,8 @@
|
||||
return [UIFont monospacedDigitSystemFontOfSize:size weight:weight];
|
||||
}
|
||||
|
||||
+ (CGFloat)cardInset { return 14; }
|
||||
+ (CGFloat)cardRadius { return 12; }
|
||||
+ (CGFloat)cardInset { return 10; }
|
||||
+ (CGFloat)cardRadius { return 10; }
|
||||
|
||||
+ (UIImage *)imageWithColor:(UIColor *)color {
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0);
|
||||
@@ -40,20 +45,30 @@
|
||||
return img;
|
||||
}
|
||||
|
||||
+ (UIImage *)roundedImageWithColor:(UIColor *)color radius:(CGFloat)radius {
|
||||
CGFloat s = radius * 2 + 2;
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(s, s), NO, 0);
|
||||
[color setFill];
|
||||
[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, s, s) cornerRadius:radius] fill];
|
||||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return [img resizableImageWithCapInsets:UIEdgeInsetsMake(radius, radius, radius, radius)];
|
||||
}
|
||||
|
||||
+ (void)applyAppearance {
|
||||
// Navigationsleiste: flach, gleiche Farbe wie der Hintergrund, keine Trennlinie
|
||||
// Navigationsleiste: gelb, schwarze fette Schrift, ohne Trennlinie -> geht nahtlos in den Kopfbereich ueber
|
||||
UINavigationBar *nav = [UINavigationBar appearance];
|
||||
[nav setBackgroundImage:[self imageWithColor:[self background]] forBarMetrics:UIBarMetricsDefault];
|
||||
[nav setBackgroundImage:[self imageWithColor:[self header]] forBarMetrics:UIBarMetricsDefault];
|
||||
nav.shadowImage = [[UIImage alloc] init];
|
||||
nav.translucent = NO;
|
||||
nav.barTintColor = [self background];
|
||||
nav.tintColor = [self textPrimary];
|
||||
nav.titleTextAttributes = @{ NSForegroundColorAttributeName: [self textPrimary],
|
||||
nav.barTintColor = [self header];
|
||||
nav.tintColor = [self headerText];
|
||||
nav.titleTextAttributes = @{ NSForegroundColorAttributeName: [self headerText],
|
||||
NSFontAttributeName: [self font:17 weight:UIFontWeightSemibold] };
|
||||
[[UIBarButtonItem appearance] setTitleTextAttributes:@{ NSFontAttributeName: [self font:17 weight:UIFontWeightRegular] }
|
||||
[[UIBarButtonItem appearance] setTitleTextAttributes:@{ NSFontAttributeName: [self font:16 weight:UIFontWeightRegular] }
|
||||
forState:UIControlStateNormal];
|
||||
|
||||
// Tab-Leiste: weiss, Auswahl dunkel
|
||||
// Tab-Leiste: weiss, aktiver Tab schwarz mit gelbem Balken
|
||||
UITabBar *tab = [UITabBar appearance];
|
||||
tab.barTintColor = [UIColor whiteColor];
|
||||
tab.tintColor = [self textPrimary];
|
||||
@@ -69,4 +84,17 @@
|
||||
[UISegmentedControl appearance].tintColor = [self textPrimary];
|
||||
}
|
||||
|
||||
+ (void)applyTabBarIndicator:(UITabBar *)bar count:(NSUInteger)count {
|
||||
if (!count) return;
|
||||
CGFloat w = floor([UIScreen mainScreen].bounds.size.width / count);
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(w, 49), NO, 0);
|
||||
[[self accent] setFill];
|
||||
[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(w * 0.2, 0, w * 0.6, 3)
|
||||
byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
|
||||
cornerRadii:CGSizeMake(2, 2)] fill];
|
||||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
bar.selectionIndicatorImage = img;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -8,3 +8,12 @@
|
||||
@property (nonatomic, assign) BOOL lastInSection;
|
||||
- (void)applyCardStyle;
|
||||
@end
|
||||
|
||||
// Mehrzeilige Zelle: Symbol links, Titel + Detail beliebig lang.
|
||||
// Die Hoehe wird selbst berechnet (iOS 9 kann das mit Standardzellen nicht zuverlaessig).
|
||||
@interface InfoCell : CardCell
|
||||
@property (nonatomic, strong, readonly) UIImageView *iconView;
|
||||
@property (nonatomic, strong, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, strong, readonly) UILabel *detailLabel;
|
||||
- (CGFloat)fittingHeightForTableWidth:(CGFloat)tableWidth;
|
||||
@end
|
||||
+99
-2
@@ -52,7 +52,7 @@
|
||||
self.selectedBackgroundView = [[CardBackgroundView alloc] init];
|
||||
self.textLabel.font = [BVGTheme font:16 weight:UIFontWeightRegular];
|
||||
self.textLabel.textColor = [BVGTheme textPrimary];
|
||||
self.detailTextLabel.font = [BVGTheme font:14 weight:UIFontWeightRegular];
|
||||
self.detailTextLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
self.detailTextLabel.textColor = [BVGTheme textSecondary];
|
||||
self.firstInSection = YES;
|
||||
self.lastInSection = YES;
|
||||
@@ -102,9 +102,106 @@
|
||||
self.textLabel.font = [BVGTheme font:16 weight:UIFontWeightRegular];
|
||||
self.textLabel.textColor = [BVGTheme textPrimary];
|
||||
self.textLabel.textAlignment = NSTextAlignmentLeft;
|
||||
self.detailTextLabel.font = [BVGTheme font:14 weight:UIFontWeightRegular];
|
||||
self.detailTextLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
self.detailTextLabel.textColor = [BVGTheme textSecondary];
|
||||
self.contentView.alpha = 1;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Mehrzeilige Zelle mit selbst berechneter Hoehe
|
||||
|
||||
@implementation InfoCell
|
||||
|
||||
static const CGFloat kPadV = 10, kPadH = 12, kGap = 10, kLineGap = 3;
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
_iconView = [[UIImageView alloc] init];
|
||||
_iconView.contentMode = UIViewContentModeLeft;
|
||||
[self.contentView addSubview:_iconView];
|
||||
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.numberOfLines = 0;
|
||||
_titleLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
_titleLabel.textColor = [BVGTheme textPrimary];
|
||||
[self.contentView addSubview:_titleLabel];
|
||||
|
||||
_detailLabel = [[UILabel alloc] init];
|
||||
_detailLabel.numberOfLines = 0;
|
||||
_detailLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
_detailLabel.textColor = [BVGTheme textSecondary];
|
||||
[self.contentView addSubview:_detailLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGFloat)iconWidth {
|
||||
UIImage *img = self.iconView.image;
|
||||
return img ? MIN(MAX(img.size.width, 24), 64) : 0;
|
||||
}
|
||||
|
||||
- (CGFloat)textX {
|
||||
CGFloat iw = [self iconWidth];
|
||||
return kPadH + (iw ? iw + kGap : 0);
|
||||
}
|
||||
|
||||
+ (CGFloat)heightOfLabel:(UILabel *)l width:(CGFloat)w {
|
||||
if (!l.text.length && !l.attributedText.length) return 0;
|
||||
if (w <= 0) return 0;
|
||||
return ceil([l sizeThatFits:CGSizeMake(w, CGFLOAT_MAX)].height);
|
||||
}
|
||||
|
||||
// Breite fuer den Text bei gegebener Tabellenbreite (Karten-Einrueckung + Pfeil rechts beruecksichtigt)
|
||||
- (CGFloat)textWidthForContentWidth:(CGFloat)cw {
|
||||
return cw - [self textX] - kPadH;
|
||||
}
|
||||
|
||||
- (CGFloat)heightForTextWidth:(CGFloat)tw {
|
||||
CGFloat th = [InfoCell heightOfLabel:self.titleLabel width:tw];
|
||||
CGFloat dh = [InfoCell heightOfLabel:self.detailLabel width:tw];
|
||||
CGFloat h = kPadV + th + (th && dh ? kLineGap : 0) + dh + kPadV;
|
||||
CGFloat iconH = self.iconView.image ? self.iconView.image.size.height + 2 * kPadV : 0;
|
||||
return ceil(MAX(MAX(h, iconH), 44));
|
||||
}
|
||||
|
||||
- (CGFloat)fittingHeightForTableWidth:(CGFloat)tableWidth {
|
||||
CGFloat cw = tableWidth - 2 * [BVGTheme cardInset];
|
||||
if (self.accessoryType != UITableViewCellAccessoryNone || self.accessoryView) cw -= 40; // Pfeil/Schalter (lieber etwas zu viel Platz als abgeschnitten)
|
||||
return [self heightForTextWidth:[self textWidthForContentWidth:cw]];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
CGFloat w = self.contentView.bounds.size.width;
|
||||
CGFloat h = self.contentView.bounds.size.height;
|
||||
CGFloat tw = [self textWidthForContentWidth:w];
|
||||
CGFloat th = [InfoCell heightOfLabel:self.titleLabel width:tw];
|
||||
CGFloat dh = [InfoCell heightOfLabel:self.detailLabel width:tw];
|
||||
CGFloat total = th + (th && dh ? kLineGap : 0) + dh;
|
||||
CGFloat y = MAX(kPadV, floor((h - total) / 2)); // vertikal mittig
|
||||
|
||||
CGFloat iw = [self iconWidth];
|
||||
if (iw) {
|
||||
CGFloat ih = self.iconView.image.size.height;
|
||||
self.iconView.frame = CGRectMake(kPadH, floor((h - ih) / 2), iw, ih);
|
||||
}
|
||||
self.titleLabel.frame = CGRectMake([self textX], y, tw, th);
|
||||
self.detailLabel.frame = CGRectMake([self textX], y + th + (th && dh ? kLineGap : 0), tw, dh);
|
||||
}
|
||||
|
||||
- (void)prepareForReuse {
|
||||
[super prepareForReuse];
|
||||
self.iconView.image = nil;
|
||||
self.titleLabel.attributedText = nil;
|
||||
self.titleLabel.text = nil;
|
||||
self.detailLabel.text = nil;
|
||||
self.titleLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
self.titleLabel.textColor = [BVGTheme textPrimary];
|
||||
self.titleLabel.textAlignment = NSTextAlignmentLeft;
|
||||
self.detailLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
self.detailLabel.textColor = [BVGTheme textSecondary];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,7 +1,7 @@
|
||||
#import "CardTableViewController.h"
|
||||
|
||||
// Ruhiges Layout: Gelb nur in der Navigationsleiste, Inhalt auf hellgrauem Grund.
|
||||
@interface CardTableViewController ()
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *subtitleLabel;
|
||||
@end
|
||||
|
||||
@@ -18,25 +18,26 @@
|
||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.tableView.sectionHeaderHeight = 0;
|
||||
self.tableView.sectionFooterHeight = 0;
|
||||
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 16, 0);
|
||||
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 12, 0);
|
||||
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 8)];
|
||||
if (self.largeTitle) [self updateLargeHeader];
|
||||
else self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 6)]; // sonst 35 pt Luft (grouped)
|
||||
[self updateLargeHeader];
|
||||
// "Zurueck" statt "Back" (bzw. statt langer Titel wie "16:01 – 18:42")
|
||||
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Zurück" style:UIBarButtonItemStylePlain
|
||||
target:nil action:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Grosser Titel
|
||||
#pragma mark - Titel & Kopf
|
||||
|
||||
- (void)setLargeTitle:(NSString *)largeTitle {
|
||||
_largeTitle = [largeTitle copy];
|
||||
if (self.isViewLoaded) [self updateLargeHeader];
|
||||
self.navigationItem.title = largeTitle; // Titel steht nur in der gelben Leiste
|
||||
}
|
||||
|
||||
- (void)setLargeSubtitle:(NSString *)largeSubtitle {
|
||||
BOOL changed = !(largeSubtitle == _largeSubtitle || [largeSubtitle isEqualToString:_largeSubtitle]);
|
||||
_largeSubtitle = [largeSubtitle copy];
|
||||
if (!self.isViewLoaded || !changed) return;
|
||||
// nur Text tauschen, wenn sich die Hoehe nicht aendert (kein Springen)
|
||||
if (self.subtitleLabel && largeSubtitle.length) {
|
||||
if (self.subtitleLabel && largeSubtitle.length) { // nur Text tauschen -> kein Springen
|
||||
self.subtitleLabel.text = largeSubtitle;
|
||||
self.subtitleLabel.textColor = self.largeSubtitleColor ?: [BVGTheme textSecondary];
|
||||
} else {
|
||||
@@ -50,61 +51,41 @@
|
||||
}
|
||||
|
||||
- (void)setHeaderAccessory:(UIView *)headerAccessory {
|
||||
if (_headerAccessory != headerAccessory) [_headerAccessory removeFromSuperview];
|
||||
_headerAccessory = headerAccessory;
|
||||
if (self.isViewLoaded) [self updateLargeHeader];
|
||||
}
|
||||
|
||||
- (void)updateLargeHeader {
|
||||
CGFloat w = self.tableView.bounds.size.width ?: [UIScreen mainScreen].bounds.size.width;
|
||||
CGFloat x = [BVGTheme cardInset] + 4;
|
||||
CGFloat x = [BVGTheme cardInset] + 6;
|
||||
UIView *header = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
CGFloat y = 2;
|
||||
CGFloat y = 8;
|
||||
|
||||
if (self.largeTitle) {
|
||||
self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(x, y, w - 2 * x, 40)];
|
||||
self.titleLabel.font = [BVGTheme font:32 weight:UIFontWeightBold];
|
||||
self.titleLabel.textColor = [BVGTheme textPrimary];
|
||||
self.titleLabel.text = self.largeTitle;
|
||||
self.titleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.titleLabel.minimumScaleFactor = 0.7;
|
||||
self.titleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[header addSubview:self.titleLabel];
|
||||
y += 40;
|
||||
}
|
||||
if (self.largeSubtitle.length) {
|
||||
self.subtitleLabel = [[UILabel alloc] initWithFrame:CGRectMake(x, y, w - 2 * x, 18)];
|
||||
self.subtitleLabel.font = [BVGTheme font:14 weight:UIFontWeightMedium];
|
||||
self.subtitleLabel.textColor = self.largeSubtitleColor ?: [BVGTheme textSecondary];
|
||||
self.subtitleLabel.text = self.largeSubtitle;
|
||||
self.subtitleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[header addSubview:self.subtitleLabel];
|
||||
y += 20;
|
||||
} else {
|
||||
self.subtitleLabel = nil;
|
||||
}
|
||||
if (self.headerAccessory) {
|
||||
y += 8;
|
||||
CGRect f = self.headerAccessory.frame;
|
||||
f.origin = CGPointMake(0, y);
|
||||
f.size.width = w;
|
||||
self.headerAccessory.frame = f;
|
||||
self.headerAccessory.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[header addSubview:self.headerAccessory];
|
||||
y += f.size.height;
|
||||
y += f.size.height + 4;
|
||||
}
|
||||
header.frame = CGRectMake(0, 0, w, y + 4);
|
||||
if (self.largeSubtitle.length) {
|
||||
self.subtitleLabel = [[UILabel alloc] initWithFrame:CGRectMake(x, y, w - 2 * x, 16)];
|
||||
self.subtitleLabel.font = [BVGTheme font:12 weight:UIFontWeightMedium];
|
||||
self.subtitleLabel.textColor = self.largeSubtitleColor ?: [BVGTheme textSecondary];
|
||||
self.subtitleLabel.text = self.largeSubtitle;
|
||||
self.subtitleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[header addSubview:self.subtitleLabel];
|
||||
y += 18;
|
||||
} else {
|
||||
self.subtitleLabel = nil;
|
||||
}
|
||||
header.frame = CGRectMake(0, 0, w, MAX(y, 6));
|
||||
self.tableView.tableHeaderView = header;
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
if (!self.largeTitle) return;
|
||||
BOOL scrolledAway = scrollView.contentOffset.y + scrollView.contentInset.top > 36;
|
||||
NSString *wanted = scrolledAway ? self.largeTitle : nil;
|
||||
if (!(wanted == self.navigationItem.title || [wanted isEqualToString:self.navigationItem.title])) {
|
||||
self.navigationItem.title = wanted;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Leerer Zustand
|
||||
|
||||
- (void)showEmptyIcon:(NSString *)icon title:(NSString *)title text:(NSString *)text {
|
||||
@@ -117,19 +98,21 @@
|
||||
NSMutableAttributedString *s = [[NSMutableAttributedString alloc] init];
|
||||
NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
|
||||
ps.alignment = NSTextAlignmentCenter;
|
||||
ps.paragraphSpacing = 6;
|
||||
ps.paragraphSpacing = 4;
|
||||
if (icon) [s appendAttributedString:[[NSAttributedString alloc] initWithString:[icon stringByAppendingString:@"\n"]
|
||||
attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:46], NSParagraphStyleAttributeName: ps }]];
|
||||
attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:34], NSParagraphStyleAttributeName: ps }]];
|
||||
if (title) [s appendAttributedString:[[NSAttributedString alloc] initWithString:[title stringByAppendingString:@"\n"]
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:19 weight:UIFontWeightSemibold],
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:16 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: [BVGTheme textPrimary], NSParagraphStyleAttributeName: ps }]];
|
||||
if (text) [s appendAttributedString:[[NSAttributedString alloc] initWithString:text
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:15 weight:UIFontWeightRegular],
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:13 weight:UIFontWeightRegular],
|
||||
NSForegroundColorAttributeName: [BVGTheme textSecondary], NSParagraphStyleAttributeName: ps }]];
|
||||
l.attributedText = s;
|
||||
CGFloat w = v.bounds.size.width - 64;
|
||||
CGSize size = [l sizeThatFits:CGSizeMake(w, CGFLOAT_MAX)];
|
||||
l.frame = CGRectMake(32, MAX(60, v.bounds.size.height * 0.38 - size.height / 2), w, size.height);
|
||||
CGFloat top = self.tableView.tableHeaderView.frame.size.height;
|
||||
CGFloat free = v.bounds.size.height - top;
|
||||
l.frame = CGRectMake(32, top + MAX(24, free * 0.35 - size.height / 2), w, size.height);
|
||||
l.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[v addSubview:l];
|
||||
self.tableView.backgroundView = v;
|
||||
@@ -139,9 +122,10 @@
|
||||
|
||||
+ (UILabel *)sectionLabelWithText:(NSString *)text {
|
||||
UILabel *l = [[UILabel alloc] init];
|
||||
l.text = text;
|
||||
l.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
l.text = [text uppercaseString];
|
||||
l.font = [BVGTheme font:12 weight:UIFontWeightSemibold];
|
||||
l.textColor = [BVGTheme textSecondary];
|
||||
l.accessibilityTraits = UIAccessibilityTraitHeader;
|
||||
return l;
|
||||
}
|
||||
|
||||
@@ -156,44 +140,44 @@
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
|
||||
if ([self headerTitleFor:section].length) return 42;
|
||||
return section == 0 ? 8 : 20;
|
||||
if ([self headerTitleFor:section].length) return 34;
|
||||
return section == 0 ? 4 : 12;
|
||||
}
|
||||
|
||||
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
|
||||
NSString *title = [self headerTitleFor:section];
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, title.length ? 42 : 8)];
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, title.length ? 34 : 4)];
|
||||
if (!title.length) return v;
|
||||
UILabel *l = [CardTableViewController sectionLabelWithText:title];
|
||||
l.frame = CGRectMake([BVGTheme cardInset] + 4, 16, v.bounds.size.width - 2 * [BVGTheme cardInset] - 8, 20);
|
||||
l.frame = CGRectMake([BVGTheme cardInset] + 6, 14, v.bounds.size.width - 2 * [BVGTheme cardInset] - 12, 16);
|
||||
l.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[v addSubview:l];
|
||||
return v;
|
||||
}
|
||||
|
||||
- (CGFloat)footerTextHeight:(NSString *)text {
|
||||
CGFloat w = self.tableView.bounds.size.width - 2 * ([BVGTheme cardInset] + 4);
|
||||
CGFloat w = self.tableView.bounds.size.width - 2 * ([BVGTheme cardInset] + 6);
|
||||
CGRect r = [text boundingRectWithSize:CGSizeMake(w, CGFLOAT_MAX)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:13 weight:UIFontWeightRegular] }
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:12 weight:UIFontWeightRegular] }
|
||||
context:nil];
|
||||
return ceil(r.size.height);
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
|
||||
NSString *text = [self footerTitleFor:section];
|
||||
return text.length ? [self footerTextHeight:text] + 16 : 0.01;
|
||||
return text.length ? [self footerTextHeight:text] + 12 : 0.01;
|
||||
}
|
||||
|
||||
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
|
||||
NSString *text = [self footerTitleFor:section];
|
||||
if (!text.length) return [[UIView alloc] initWithFrame:CGRectZero];
|
||||
CGFloat h = [self footerTextHeight:text];
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, h + 16)];
|
||||
UILabel *l = [[UILabel alloc] initWithFrame:CGRectMake([BVGTheme cardInset] + 4, 8,
|
||||
v.bounds.size.width - 2 * ([BVGTheme cardInset] + 4), h)];
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, h + 12)];
|
||||
UILabel *l = [[UILabel alloc] initWithFrame:CGRectMake([BVGTheme cardInset] + 6, 6,
|
||||
v.bounds.size.width - 2 * ([BVGTheme cardInset] + 6), h)];
|
||||
l.numberOfLines = 0;
|
||||
l.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
l.font = [BVGTheme font:12 weight:UIFontWeightRegular];
|
||||
l.textColor = [BVGTheme textSecondary];
|
||||
l.text = text;
|
||||
l.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
|
||||
+4
-4
@@ -21,10 +21,10 @@
|
||||
for (NSUInteger i = 0; i < titles.count; i++) {
|
||||
UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[b setTitle:titles[i] forState:UIControlStateNormal];
|
||||
b.titleLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
CGFloat w = ceil([titles[i] sizeWithAttributes:@{ NSFontAttributeName: b.titleLabel.font }].width) + 28;
|
||||
b.frame = CGRectMake(x, 6, w, 32);
|
||||
b.layer.cornerRadius = 16;
|
||||
b.titleLabel.font = [BVGTheme font:14 weight:UIFontWeightMedium];
|
||||
CGFloat w = ceil([titles[i] sizeWithAttributes:@{ NSFontAttributeName: b.titleLabel.font }].width) + 24;
|
||||
b.frame = CGRectMake(x, 7, w, 30);
|
||||
b.layer.cornerRadius = 15;
|
||||
b.tag = i;
|
||||
[b addTarget:self action:@selector(tapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_scroll addSubview:b];
|
||||
|
||||
+40
-30
@@ -2,6 +2,7 @@
|
||||
#import "BVGAPI.h"
|
||||
#import "BVGTheme.h"
|
||||
|
||||
// Abfahrt im Stil der BVG-Anzeigen: Linie | Richtung (fett) + Infozeile | grosse Minutenzahl
|
||||
@interface DepartureCell ()
|
||||
@property (nonatomic, strong) UIImageView *badgeView;
|
||||
@property (nonatomic, strong) UILabel *directionLabel;
|
||||
@@ -13,7 +14,7 @@
|
||||
|
||||
@implementation DepartureCell
|
||||
|
||||
+ (CGFloat)height { return 64; }
|
||||
+ (CGFloat)height { return 56; }
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
|
||||
@@ -23,12 +24,14 @@
|
||||
[self.contentView addSubview:_badgeView];
|
||||
|
||||
_directionLabel = [[UILabel alloc] init];
|
||||
_directionLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
_directionLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
_directionLabel.textColor = [BVGTheme textPrimary];
|
||||
_directionLabel.adjustsFontSizeToFitWidth = YES;
|
||||
_directionLabel.minimumScaleFactor = 0.85;
|
||||
[self.contentView addSubview:_directionLabel];
|
||||
|
||||
_infoLabel = [[UILabel alloc] init];
|
||||
_infoLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
_infoLabel.font = [BVGTheme font:12 weight:UIFontWeightRegular];
|
||||
[self.contentView addSubview:_infoLabel];
|
||||
|
||||
_minutesLabel = [[UILabel alloc] init];
|
||||
@@ -36,13 +39,16 @@
|
||||
[self.contentView addSubview:_minutesLabel];
|
||||
|
||||
_subLabel = [[UILabel alloc] init];
|
||||
_subLabel.font = [BVGTheme digits:12 weight:UIFontWeightMedium];
|
||||
_subLabel.font = [BVGTheme digits:11 weight:UIFontWeightMedium];
|
||||
_subLabel.adjustsFontSizeToFitWidth = YES;
|
||||
_subLabel.minimumScaleFactor = 0.8;
|
||||
_subLabel.textAlignment = NSTextAlignmentRight;
|
||||
[self.contentView addSubview:_subLabel];
|
||||
|
||||
_emptyLabel = [[UILabel alloc] init];
|
||||
_emptyLabel.font = [BVGTheme font:15 weight:UIFontWeightRegular];
|
||||
_emptyLabel.font = [BVGTheme font:14 weight:UIFontWeightRegular];
|
||||
_emptyLabel.textColor = [BVGTheme textSecondary];
|
||||
_emptyLabel.numberOfLines = 2;
|
||||
_emptyLabel.hidden = YES;
|
||||
[self.contentView addSubview:_emptyLabel];
|
||||
}
|
||||
@@ -53,19 +59,21 @@
|
||||
[super layoutSubviews];
|
||||
CGFloat w = self.contentView.bounds.size.width;
|
||||
CGFloat h = self.contentView.bounds.size.height;
|
||||
CGFloat right = 76;
|
||||
CGFloat textX = 80;
|
||||
CGFloat right = 70;
|
||||
CGFloat badgeW = MAX(46, MIN(self.badgeView.image.size.width, 60));
|
||||
CGFloat textX = 12 + badgeW + 10;
|
||||
CGFloat textW = w - textX - right - 14;
|
||||
|
||||
self.badgeView.frame = CGRectMake(14, (h - 24) / 2, 62, 24);
|
||||
self.directionLabel.frame = CGRectMake(textX, 12, w - textX - right - 6, 20);
|
||||
self.infoLabel.frame = CGRectMake(textX, 34, w - textX - right - 6, 17);
|
||||
self.minutesLabel.frame = CGRectMake(w - right - 14, 9, right, 28);
|
||||
self.subLabel.frame = CGRectMake(w - right - 14, 37, right, 16);
|
||||
self.badgeView.frame = CGRectMake(12, (h - 22) / 2, badgeW, 22);
|
||||
self.directionLabel.frame = CGRectMake(textX, 10, textW, 19);
|
||||
self.infoLabel.frame = CGRectMake(textX, 30, textW, 16);
|
||||
self.minutesLabel.frame = CGRectMake(w - right - 10, 8, right, 24);
|
||||
self.subLabel.frame = CGRectMake(w - right - 10, 31, right, 15);
|
||||
self.emptyLabel.frame = CGRectMake(16, 0, w - 32, h);
|
||||
}
|
||||
|
||||
- (void)setDimmed:(BOOL)dimmed {
|
||||
self.contentView.alpha = dimmed ? 0.38 : 1.0;
|
||||
self.contentView.alpha = dimmed ? 0.35 : 1.0;
|
||||
}
|
||||
|
||||
- (void)setAllHidden:(BOOL)hidden {
|
||||
@@ -85,36 +93,36 @@
|
||||
[self setAllHidden:NO];
|
||||
NSDictionary *line = NN(d[@"line"]);
|
||||
NSString *tripId = NN(d[@"tripId"]);
|
||||
NSString *dir = NN(d[@"direction"]) ?: @"";
|
||||
NSString *dir = [BVGAPI shortName:NN(d[@"direction"])] ?: @"";
|
||||
BOOL cancelled = [NN(d[@"cancelled"]) boolValue];
|
||||
NSDate *when = [BVGAPI whenOfDeparture:d];
|
||||
|
||||
self.badgeView.image = [BVGAPI badgeForLine:line];
|
||||
self.selectionStyle = tripId ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone;
|
||||
[self setNeedsLayout];
|
||||
|
||||
// Richtung
|
||||
if (cancelled) {
|
||||
self.directionLabel.attributedText = [[NSAttributedString alloc] initWithString:dir attributes:@{
|
||||
NSStrikethroughStyleAttributeName: @(NSUnderlineStyleSingle),
|
||||
NSFontAttributeName: [BVGTheme font:16 weight:UIFontWeightSemibold],
|
||||
NSFontAttributeName: [BVGTheme font:15 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: [BVGTheme textTertiary] }];
|
||||
} else {
|
||||
self.directionLabel.attributedText = nil;
|
||||
self.directionLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
self.directionLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
self.directionLabel.textColor = [BVGTheme textPrimary];
|
||||
self.directionLabel.text = dir;
|
||||
}
|
||||
|
||||
// Infozeile: Wecker, Gleis/Gleiswechsel, Stoerung
|
||||
// Infozeile: Ausfall, Stoerung, Gleis(-wechsel), Wecker
|
||||
NSMutableArray *info = [NSMutableArray array];
|
||||
UIColor *infoColor = [BVGTheme textSecondary];
|
||||
if ([BVGAPI hasAlarmForTrip:tripId]) [info addObject:@"⏰ Wecker"];
|
||||
|
||||
NSString *platform = NN(d[@"platform"]);
|
||||
NSString *planned = NN(d[@"plannedPlatform"]);
|
||||
NSString *platform = [BVGAPI platform:NN(d[@"platform"])];
|
||||
NSString *planned = [BVGAPI platform:NN(d[@"plannedPlatform"])];
|
||||
if (platform && planned && ![platform isEqualToString:planned]) {
|
||||
[info addObject:[NSString stringWithFormat:@"Gleis %@ statt %@", platform, planned]];
|
||||
infoColor = [BVGTheme orange];
|
||||
infoColor = [BVGTheme warningText];
|
||||
} else if (platform ?: planned) {
|
||||
[info addObject:[NSString stringWithFormat:@"Gleis %@", platform ?: planned]];
|
||||
}
|
||||
@@ -123,25 +131,27 @@
|
||||
if ([remarks isKindOfClass:[NSArray class]]) {
|
||||
for (NSDictionary *r in remarks) {
|
||||
if ([r isKindOfClass:[NSDictionary class]] && [NN(r[@"type"]) isEqualToString:@"warning"]) {
|
||||
[info addObject:@"⚠︎ Störung"];
|
||||
infoColor = [BVGTheme orange];
|
||||
[info insertObject:@"⚠︎ Störung" atIndex:0];
|
||||
infoColor = [BVGTheme warningText];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ([BVGAPI hasAlarmForTrip:tripId]) [info addObject:@"⏰ Wecker"];
|
||||
if (cancelled) { [info insertObject:@"Fällt aus" atIndex:0]; infoColor = [BVGTheme red]; }
|
||||
if (!info.count && when) [info addObject:[NSString stringWithFormat:@"Abfahrt %@", [BVGAPI hhmm:when]]];
|
||||
self.infoLabel.text = [info componentsJoinedByString:@" · "];
|
||||
if (!info.count && when) [info addObject:[NSString stringWithFormat:@"ab %@", [BVGAPI hhmm:when]]];
|
||||
self.infoLabel.text = [info componentsJoinedByString:@" · "];
|
||||
self.infoLabel.textColor = infoColor;
|
||||
|
||||
// rechte Spalte
|
||||
if (cancelled) {
|
||||
self.minutesLabel.attributedText = nil;
|
||||
self.minutesLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
self.minutesLabel.font = [BVGTheme font:14 weight:UIFontWeightSemibold];
|
||||
self.minutesLabel.textColor = [BVGTheme red];
|
||||
self.minutesLabel.text = @"Ausfall";
|
||||
self.subLabel.attributedText = [[NSAttributedString alloc] initWithString:[BVGAPI hhmm:when] attributes:@{
|
||||
NSStrikethroughStyleAttributeName: @(NSUnderlineStyleSingle),
|
||||
NSFontAttributeName: [BVGTheme digits:11 weight:UIFontWeightMedium],
|
||||
NSForegroundColorAttributeName: [BVGTheme textTertiary] }];
|
||||
self.accessibilityLabel = [NSString stringWithFormat:@"%@ Richtung %@ fällt aus", [BVGAPI spokenLineName:line], dir];
|
||||
return;
|
||||
@@ -154,21 +164,21 @@
|
||||
|
||||
if (mins <= 0) {
|
||||
self.minutesLabel.attributedText = nil;
|
||||
self.minutesLabel.font = [BVGTheme font:18 weight:UIFontWeightBold];
|
||||
self.minutesLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
self.minutesLabel.textColor = delay > 0 ? [BVGTheme red] : [BVGTheme green];
|
||||
self.minutesLabel.text = @"jetzt";
|
||||
} else if (mins >= 60) {
|
||||
self.minutesLabel.attributedText = nil;
|
||||
self.minutesLabel.font = [BVGTheme digits:20 weight:UIFontWeightSemibold];
|
||||
self.minutesLabel.font = [BVGTheme digits:17 weight:UIFontWeightSemibold];
|
||||
self.minutesLabel.textColor = numberColor;
|
||||
self.minutesLabel.text = [BVGAPI hhmm:when];
|
||||
} else {
|
||||
NSMutableAttributedString *s = [[NSMutableAttributedString alloc]
|
||||
initWithString:[NSString stringWithFormat:@"%ld", (long)mins]
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme digits:24 weight:UIFontWeightSemibold],
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme digits:20 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: numberColor }];
|
||||
[s appendAttributedString:[[NSAttributedString alloc] initWithString:@" min" attributes:@{
|
||||
NSFontAttributeName: [BVGTheme font:13 weight:UIFontWeightMedium],
|
||||
NSFontAttributeName: [BVGTheme font:12 weight:UIFontWeightRegular],
|
||||
NSForegroundColorAttributeName: [BVGTheme textSecondary] }]];
|
||||
self.minutesLabel.attributedText = s;
|
||||
}
|
||||
|
||||
@@ -64,8 +64,6 @@
|
||||
self.largeTitle = @"Hinweise";
|
||||
NSInteger n = [DisruptionsViewController countInCollection:self.stops];
|
||||
self.largeSubtitle = n == 1 ? @"1 aktuelle Meldung" : [NSString stringWithFormat:@"%ld aktuelle Meldungen", (long)n];
|
||||
self.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableView.estimatedRowHeight = 80;
|
||||
if (!self.stops.count) [self showEmptyIcon:@"✅" title:@"Alles ruhig" text:@"Keine Störungen an deinen Haltestellen."];
|
||||
}
|
||||
|
||||
@@ -74,31 +72,37 @@
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return self.stops[section][@"name"];
|
||||
return [BVGAPI shortName:self.stops[section][@"name"]];
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return [self.stops[section][@"items"] count];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
CardCell *cell = [[CardCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil];
|
||||
- (InfoCell *)cellAt:(NSIndexPath *)indexPath {
|
||||
InfoCell *cell = [[InfoCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
|
||||
NSDictionary *item = self.stops[indexPath.section][@"items"][indexPath.row];
|
||||
NSArray *lines = [item[@"lines"] array];
|
||||
|
||||
cell.textLabel.text = [@"⚠︎ " stringByAppendingString:item[@"summary"]];
|
||||
cell.textLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
cell.textLabel.numberOfLines = 0;
|
||||
cell.titleLabel.text = [@"⚠︎ " stringByAppendingString:item[@"summary"]];
|
||||
cell.titleLabel.textColor = [BVGTheme warningText];
|
||||
|
||||
NSString *body = [DisruptionsViewController plain:item[@"text"]];
|
||||
if (body.length > 160) body = [[body substringToIndex:157] stringByAppendingString:@" …"];
|
||||
NSString *lineText = lines.count ? [NSString stringWithFormat:@"Betrifft: %@\n", [lines componentsJoinedByString:@", "]] : @"";
|
||||
cell.detailTextLabel.text = [lineText stringByAppendingString:body];
|
||||
cell.detailTextLabel.numberOfLines = 0;
|
||||
if (body.length > 140) body = [[body substringToIndex:137] stringByAppendingString:@" …"];
|
||||
NSString *lineText = lines.count ? [NSString stringWithFormat:@"Betrifft %@ · ", [lines componentsJoinedByString:@", "]] : @"";
|
||||
cell.detailLabel.text = [lineText stringByAppendingString:body];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [[self cellAt:indexPath] fittingHeightForTableWidth:tableView.bounds.size.width];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [self cellAt:indexPath];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
NSDictionary *item = self.stops[indexPath.section][@"items"][indexPath.row];
|
||||
|
||||
@@ -9,6 +9,7 @@ typedef NS_ENUM(NSInteger, GuidanceStatus) {
|
||||
};
|
||||
|
||||
extern NSString * const GuidanceChangedNotification;
|
||||
extern NSString * const GuidanceAlternativeCategory; // Mitteilungs-Kategorie mit Knoepfen
|
||||
|
||||
// Begleitet eine Verbindung: Ein-/Aussteige-Erinnerungen, regelmaessige Echtzeit-Pruefung,
|
||||
// Alternativsuche bei Problemen. Die Entscheidung trifft immer der Nutzer.
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
NSString * const GuidanceChangedNotification = @"GuidanceChanged";
|
||||
|
||||
static NSString * const kSavedJourneyKey = @"guidanceJourney";
|
||||
static NSString * const kSavedAltKey = @"guidanceAlternative"; // fuer "Uebernehmen" direkt aus der Mitteilung
|
||||
NSString * const GuidanceAlternativeCategory = @"ALTERNATIVE";
|
||||
static const NSTimeInterval kCheckInterval = 60; // Echtzeit-Pruefung jede Minute
|
||||
#define kBoardLead ((NSTimeInterval)[BVGSettings boardLeadMinutes] * 60) // Einsteigen-Hinweis (Einstellung)
|
||||
static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per GPS-Naehe
|
||||
@@ -25,6 +27,7 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
@property (nonatomic, strong) NSTimer *timer;
|
||||
@property (nonatomic, strong) CLLocationManager *locationManager;
|
||||
@property (nonatomic, strong) CLLocation *lastLocation;
|
||||
@property (nonatomic, assign) BOOL preciseLocation;
|
||||
@end
|
||||
|
||||
@implementation GuidanceManager
|
||||
@@ -107,6 +110,14 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
[self changed];
|
||||
}
|
||||
|
||||
// Alternative wird mitgespeichert: so funktioniert "Uebernehmen" auch, wenn iOS die App zwischendurch beendet hat
|
||||
- (void)setAlternative:(NSDictionary *)alternative {
|
||||
_alternative = alternative;
|
||||
NSData *data = alternative ? [NSJSONSerialization dataWithJSONObject:alternative options:0 error:nil] : nil;
|
||||
if (data) [[NSUserDefaults standardUserDefaults] setObject:data forKey:kSavedAltKey];
|
||||
else [[NSUserDefaults standardUserDefaults] removeObjectForKey:kSavedAltKey];
|
||||
}
|
||||
|
||||
- (void)save {
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:self.journey options:0 error:nil];
|
||||
if (data) [[NSUserDefaults standardUserDefaults] setObject:data forKey:kSavedJourneyKey];
|
||||
@@ -125,6 +136,11 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
self.journey = j;
|
||||
self.active = YES;
|
||||
self.exitAlertedLegs = [NSMutableSet set];
|
||||
NSData *altData = [[NSUserDefaults standardUserDefaults] objectForKey:kSavedAltKey];
|
||||
if ([altData isKindOfClass:[NSData class]]) {
|
||||
NSDictionary *alt = [NSJSONSerialization JSONObjectWithData:altData options:0 error:nil];
|
||||
if ([alt isKindOfClass:[NSDictionary class]]) _alternative = alt;
|
||||
}
|
||||
[self startMonitoring];
|
||||
[self checkNow];
|
||||
}
|
||||
@@ -151,13 +167,40 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) {
|
||||
[self.locationManager requestWhenInUseAuthorization];
|
||||
}
|
||||
self.preciseLocation = YES; // erzwingt die erste Einstellung
|
||||
[self updateLocationAccuracy];
|
||||
[self.locationManager startUpdatingLocation];
|
||||
}
|
||||
|
||||
// Akku sparen: genaues GPS nur, wenn es gebraucht wird (zu Fuss, kurz vor dem Einsteigen oder Aussteigen).
|
||||
// Sonst grobe Ortung ueber Funkzellen - die App laeuft trotzdem im Hintergrund weiter und prueft jede Minute.
|
||||
- (BOOL)needsPreciseLocation {
|
||||
NSArray *legs = [self legs];
|
||||
NSInteger idx = [self currentLegIndex];
|
||||
if (idx >= (NSInteger)legs.count) return NO;
|
||||
NSDictionary *leg = legs[idx];
|
||||
if ([GuidanceManager isWalk:leg]) return YES;
|
||||
NSDate *dep = [GuidanceManager legDeparture:leg];
|
||||
NSDate *arr = [GuidanceManager legArrival:leg];
|
||||
if (dep && [dep timeIntervalSinceNow] < 5 * 60 && [dep timeIntervalSinceNow] > -60) return YES; // Einsteigen
|
||||
if (arr && [arr timeIntervalSinceNow] < 6 * 60) return YES; // Aussteigen
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)updateLocationAccuracy {
|
||||
if (!self.locationManager) return;
|
||||
BOOL precise = [self needsPreciseLocation];
|
||||
if (precise == self.preciseLocation && self.locationManager.distanceFilter > 0) return;
|
||||
self.preciseLocation = precise;
|
||||
self.locationManager.desiredAccuracy = precise ? kCLLocationAccuracyHundredMeters : kCLLocationAccuracyThreeKilometers;
|
||||
self.locationManager.distanceFilter = precise ? 30 : 1000;
|
||||
}
|
||||
|
||||
#pragma mark - Pruefen
|
||||
|
||||
- (void)checkNow {
|
||||
if (!self.active || self.checking) return;
|
||||
[self updateLocationAccuracy];
|
||||
self.checking = YES;
|
||||
[self changed];
|
||||
|
||||
@@ -331,7 +374,7 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
NSInteger diff = (currentArrival && bestArr) ? (NSInteger)round([bestArr timeIntervalSinceDate:currentArrival] / 60.0) : 0;
|
||||
NSMutableArray *lines = [NSMutableArray array];
|
||||
for (NSDictionary *l in best[@"legs"]) if (![GuidanceManager isWalk:l]) [lines addObject:NN(l[@"line"][@"name"]) ?: @"?"];
|
||||
NSString *text = [NSString stringWithFormat:@"⚠ %@ Alternative: %@, Ankunft %@%@. Antippen zum Entscheiden.",
|
||||
NSString *text = [NSString stringWithFormat:@"⚠ %@ Alternative: %@, Ankunft %@%@. Wischen oder antippen zum Entscheiden.",
|
||||
problem, [lines componentsJoinedByString:@" → "], [BVGAPI hhmm:bestArr],
|
||||
diff > 0 ? [NSString stringWithFormat:@" (+%ld min)", (long)diff] : @""];
|
||||
[self notifyNow:text kind:@"alternative"];
|
||||
@@ -364,6 +407,7 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
}
|
||||
|
||||
- (NSString *)platformText:(NSString *)platform {
|
||||
platform = [BVGAPI platform:platform];
|
||||
return platform ? [NSString stringWithFormat:@", Gleis %@", platform] : @"";
|
||||
}
|
||||
|
||||
@@ -408,6 +452,7 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
n.alertBody = text;
|
||||
n.soundName = UILocalNotificationDefaultSoundName; // bei Stumm: nur Vibration (System)
|
||||
n.userInfo = @{ @"kind": kind };
|
||||
if ([kind isEqualToString:@"alternative"]) n.category = GuidanceAlternativeCategory; // Knoepfe Uebernehmen/Behalten
|
||||
[[UIApplication sharedApplication] presentLocalNotificationNow:n];
|
||||
}
|
||||
|
||||
@@ -470,6 +515,7 @@ static const CLLocationDistance kExitRadius = 400; // Backup: Aussteigen per G
|
||||
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
|
||||
self.lastLocation = locations.lastObject;
|
||||
if (!self.active) return;
|
||||
[self updateLocationAccuracy];
|
||||
|
||||
// Hintergrund: falls der Timer nicht feuert, haelt der Standort die Pruefung am Laufen
|
||||
if (!self.lastCheck || [self.lastCheck timeIntervalSinceNow] < -kCheckInterval) [self checkNow];
|
||||
|
||||
@@ -15,13 +15,10 @@ typedef NS_ENUM(NSInteger, GSection) { GSectionStatus, GSectionAlternative, GSec
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.navigationItem.title = @"Navigation";
|
||||
self.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableView.estimatedRowHeight = 48;
|
||||
|
||||
UIBarButtonItem *check = [[UIBarButtonItem alloc] initWithTitle:@"Prüfen" style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(check)];
|
||||
UIBarButtonItem *speak = [[UIBarButtonItem alloc] initWithTitle:@"Ansage" style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(speak)];
|
||||
UIBarButtonItem *check = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
|
||||
target:self action:@selector(check)];
|
||||
UIBarButtonItem *speak = [BVGAPI barButtonWithIcon:@"speaker" target:self action:@selector(speak)];
|
||||
speak.accessibilityLabel = @"Anweisung ansagen";
|
||||
self.navigationItem.rightBarButtonItems = @[check, speak];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload)
|
||||
@@ -140,47 +137,54 @@ typedef NS_ENUM(NSInteger, GSection) { GSectionStatus, GSectionAlternative, GSec
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
CardCell *cell = [[CardCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil];
|
||||
+ (NSString *)name:(NSDictionary *)place {
|
||||
return [BVGAPI shortName:NN(NN(place)[@"name"])] ?: @"";
|
||||
}
|
||||
|
||||
// "18:05 (+2)" bzw. "18:05"
|
||||
+ (NSString *)time:(NSDate *)t delay:(id)delaySeconds {
|
||||
NSInteger d = [NN(delaySeconds) integerValue] / 60;
|
||||
return d > 0 ? [NSString stringWithFormat:@"%@ (+%ld)", [BVGAPI hhmm:t], (long)d] : [BVGAPI hhmm:t];
|
||||
}
|
||||
|
||||
- (InfoCell *)cellAt:(NSIndexPath *)indexPath {
|
||||
InfoCell *cell = [[InfoCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
cell.textLabel.numberOfLines = 0;
|
||||
cell.detailTextLabel.numberOfLines = 0;
|
||||
GuidanceManager *g = [GuidanceManager shared];
|
||||
NSArray *legs = [self legs];
|
||||
|
||||
switch ([self kind:indexPath.section]) {
|
||||
case GSectionStatus: {
|
||||
if (indexPath.row == 0) {
|
||||
cell.textLabel.text = [g currentInstruction];
|
||||
cell.textLabel.font = [BVGTheme font:20 weight:UIFontWeightSemibold];
|
||||
cell.cardColor = [BVGTheme accent];
|
||||
cell.titleLabel.text = [BVGAPI shortName:[g currentInstruction]];
|
||||
cell.titleLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
cell.titleLabel.textColor = [UIColor whiteColor];
|
||||
cell.cardColor = [BVGTheme textPrimary]; // aktuelle Anweisung hervorgehoben
|
||||
} else if (indexPath.row == 1) {
|
||||
switch (g.status) {
|
||||
case GuidanceStatusOK:
|
||||
cell.textLabel.text = @"✓ Alle Anschlüsse erreichbar";
|
||||
cell.textLabel.textColor = [BVGTheme green];
|
||||
cell.titleLabel.text = @"✓ Alle Anschlüsse erreichbar";
|
||||
cell.titleLabel.textColor = [BVGTheme green];
|
||||
break;
|
||||
case GuidanceStatusAtRisk:
|
||||
cell.textLabel.text = [@"⚠ " stringByAppendingString:g.problemText ?: @"Umstieg knapp"];
|
||||
cell.textLabel.textColor = [BVGTheme orange];
|
||||
cell.titleLabel.text = [@"⚠︎ " stringByAppendingString:g.problemText ?: @"Umstieg knapp"];
|
||||
cell.titleLabel.textColor = [BVGTheme warningText];
|
||||
break;
|
||||
case GuidanceStatusBroken:
|
||||
cell.textLabel.text = [@"✕ " stringByAppendingString:g.problemText ?: @"Route nicht mehr möglich"];
|
||||
cell.textLabel.textColor = [BVGTheme red];
|
||||
cell.titleLabel.text = [@"✕ " stringByAppendingString:g.problemText ?: @"Route nicht mehr möglich"];
|
||||
cell.titleLabel.textColor = [BVGTheme red];
|
||||
break;
|
||||
case GuidanceStatusDone:
|
||||
cell.textLabel.text = @"🎉 Ziel erreicht";
|
||||
cell.titleLabel.text = @"Ziel erreicht";
|
||||
break;
|
||||
}
|
||||
cell.textLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
cell.titleLabel.font = [BVGTheme font:14 weight:UIFontWeightSemibold];
|
||||
} else {
|
||||
NSDictionary *last = legs.lastObject;
|
||||
NSDate *arr = [GuidanceManager legArrival:last];
|
||||
NSInteger delay = [NN(last[@"arrivalDelay"]) integerValue] / 60;
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"Ankunft %@%@", [BVGAPI hhmm:arr],
|
||||
delay > 0 ? [NSString stringWithFormat:@" (+%ld)", (long)delay] : @""];
|
||||
cell.detailTextLabel.text = NN(NN(last[@"destination"])[@"name"]);
|
||||
cell.textLabel.font = [BVGTheme digits:17 weight:UIFontWeightMedium];
|
||||
cell.titleLabel.text = [NSString stringWithFormat:@"Ankunft %@",
|
||||
[GuidanceViewController time:[GuidanceManager legArrival:last] delay:last[@"arrivalDelay"]]];
|
||||
cell.titleLabel.font = [BVGTheme digits:14 weight:UIFontWeightSemibold];
|
||||
cell.detailLabel.text = [GuidanceViewController name:last[@"destination"]];
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
@@ -190,25 +194,20 @@ typedef NS_ENUM(NSInteger, GSection) { GSectionStatus, GSectionAlternative, GSec
|
||||
if (indexPath.row == 0) {
|
||||
NSMutableArray *lines = [NSMutableArray array];
|
||||
for (NSDictionary *l in alt) if (![GuidanceManager isWalk:l]) [lines addObject:l[@"line"]];
|
||||
cell.imageView.image = [BVGAPI badgeStripForLines:lines];
|
||||
cell.iconView.image = [BVGAPI badgeStripForLines:lines];
|
||||
NSDate *dep = [GuidanceManager legDeparture:alt.firstObject];
|
||||
NSDate *arr = [GuidanceManager legArrival:alt.lastObject];
|
||||
NSDate *oldArr = [GuidanceManager legArrival:legs.lastObject];
|
||||
NSInteger diff = (arr && oldArr) ? (NSInteger)round([arr timeIntervalSinceDate:oldArr] / 60.0) : 0;
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@ → %@", [BVGAPI hhmm:dep], [BVGAPI hhmm:arr]];
|
||||
cell.textLabel.font = [BVGTheme digits:17 weight:UIFontWeightSemibold];
|
||||
cell.detailTextLabel.text = diff > 0 ? [NSString stringWithFormat:@"%ld min später als geplant", (long)diff]
|
||||
: @"Ankunft wie geplant oder früher";
|
||||
} else if (indexPath.row == 1) {
|
||||
cell.textLabel.text = @"✓ Alternative übernehmen";
|
||||
cell.textLabel.textColor = [BVGTheme green];
|
||||
cell.textLabel.font = [BVGTheme font:17 weight:UIFontWeightSemibold];
|
||||
cell.textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
cell.titleLabel.text = [NSString stringWithFormat:@"%@ – %@", [BVGAPI hhmm:dep], [BVGAPI hhmm:arr]];
|
||||
cell.titleLabel.font = [BVGTheme digits:15 weight:UIFontWeightSemibold];
|
||||
cell.detailLabel.text = diff > 0 ? [NSString stringWithFormat:@"%ld min später als geplant", (long)diff]
|
||||
: @"Ankunft wie geplant oder früher";
|
||||
} else {
|
||||
cell.textLabel.text = @"Aktuelle Route behalten";
|
||||
cell.textLabel.textColor = [BVGTheme textSecondary];
|
||||
cell.textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
BOOL accept = indexPath.row == 1;
|
||||
cell.titleLabel.text = accept ? @"Alternative übernehmen" : @"Aktuelle Route behalten";
|
||||
cell.titleLabel.textColor = accept ? [BVGTheme green] : [BVGTheme textSecondary];
|
||||
cell.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
}
|
||||
return cell;
|
||||
@@ -220,46 +219,49 @@ typedef NS_ENUM(NSInteger, GSection) { GSectionStatus, GSectionAlternative, GSec
|
||||
BOOL done = indexPath.row < current;
|
||||
NSDate *dep = [GuidanceManager legDeparture:leg];
|
||||
NSDate *arr = [GuidanceManager legArrival:leg];
|
||||
NSString *origin = NN(NN(leg[@"origin"])[@"name"]) ?: @"";
|
||||
NSString *dest = NN(NN(leg[@"destination"])[@"name"]) ?: @"";
|
||||
NSString *origin = [GuidanceViewController name:leg[@"origin"]];
|
||||
NSString *dest = [GuidanceViewController name:leg[@"destination"]];
|
||||
|
||||
if ([GuidanceManager isWalk:leg]) {
|
||||
NSInteger mins = (dep && arr) ? (NSInteger)round([arr timeIntervalSinceDate:dep] / 60.0) : 0;
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"🚶 %ld min zu Fuß", (long)mins];
|
||||
cell.detailTextLabel.text = [@"bis " stringByAppendingString:dest];
|
||||
if (mins <= 1 && ([origin isEqualToString:dest] || !NN(leg[@"distance"]) || [NN(leg[@"distance"]) integerValue] < 80)) {
|
||||
cell.titleLabel.text = @"Umsteigen";
|
||||
cell.detailLabel.text = [@"am Bahnhof " stringByAppendingString:dest];
|
||||
} else {
|
||||
cell.titleLabel.text = [NSString stringWithFormat:@"🚶 %ld min zu Fuß", (long)MAX(mins, 1)];
|
||||
cell.detailLabel.text = [@"bis " stringByAppendingString:dest];
|
||||
}
|
||||
cell.titleLabel.font = [BVGTheme font:14 weight:UIFontWeightMedium];
|
||||
} else {
|
||||
cell.imageView.image = [BVGAPI badgeForLine:leg[@"line"]];
|
||||
NSInteger dd = [NN(leg[@"departureDelay"]) integerValue] / 60;
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@%@ %@ → %@", [BVGAPI hhmm:dep],
|
||||
dd > 0 ? [NSString stringWithFormat:@" (+%ld)", (long)dd] : @"",
|
||||
origin, NN(leg[@"direction"]) ?: @""];
|
||||
NSString *dp = NN(leg[@"departurePlatform"]), *ap = NN(leg[@"arrivalPlatform"]);
|
||||
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@%@ aussteigen: %@ um %@%@",
|
||||
dp ? [NSString stringWithFormat:@"Gl. %@ · ", dp] : @"",
|
||||
[NN(leg[@"cancelled"]) boolValue] ? @"FÄLLT AUS · " : @"",
|
||||
dest, [BVGAPI hhmm:arr], ap ? [NSString stringWithFormat:@" (Gl. %@)", ap] : @""];
|
||||
cell.iconView.image = [BVGAPI badgeForLine:leg[@"line"]];
|
||||
cell.titleLabel.text = [@"→ " stringByAppendingString:[BVGAPI shortName:NN(leg[@"direction"])] ?: @""];
|
||||
cell.titleLabel.font = [BVGTheme font:15 weight:(indexPath.row == current ? UIFontWeightSemibold : UIFontWeightRegular)];
|
||||
|
||||
NSString *dp = [BVGAPI platform:NN(leg[@"departurePlatform"])], *ap = [BVGAPI platform:NN(leg[@"arrivalPlatform"])];
|
||||
NSMutableString *d = [NSMutableString string];
|
||||
if ([NN(leg[@"cancelled"]) boolValue]) [d appendString:@"FÄLLT AUS\n"];
|
||||
[d appendFormat:@"ab %@ %@%@", [GuidanceViewController time:dep delay:leg[@"departureDelay"]], origin,
|
||||
dp ? [NSString stringWithFormat:@" · Gl. %@", dp] : @""];
|
||||
[d appendFormat:@"\nan %@ %@%@", [GuidanceViewController time:arr delay:leg[@"arrivalDelay"]], dest,
|
||||
ap ? [NSString stringWithFormat:@" · Gl. %@", ap] : @""];
|
||||
cell.detailLabel.text = d;
|
||||
cell.detailLabel.font = [BVGTheme digits:13 weight:UIFontWeightRegular];
|
||||
cell.accessoryType = NN(leg[@"tripId"]) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
}
|
||||
cell.textLabel.font = indexPath.row == current ? [BVGTheme font:15 weight:UIFontWeightSemibold] : [BVGTheme font:15 weight:UIFontWeightRegular];
|
||||
cell.textLabel.textColor = done ? [BVGTheme textTertiary] : [BVGTheme textPrimary];
|
||||
cell.detailTextLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
cell.detailTextLabel.textColor = [NN(leg[@"cancelled"]) boolValue] ? [BVGTheme red] : [BVGTheme textSecondary];
|
||||
cell.titleLabel.textColor = done ? [BVGTheme textTertiary] : [BVGTheme textPrimary];
|
||||
cell.detailLabel.textColor = [NN(leg[@"cancelled"]) boolValue] ? [BVGTheme red] : [BVGTheme textSecondary];
|
||||
if (indexPath.row == current) cell.cardColor = [BVGTheme accentSoft];
|
||||
if (done) cell.contentView.alpha = 0.5;
|
||||
return cell;
|
||||
}
|
||||
|
||||
case GSectionEnd: {
|
||||
if (indexPath.row == 0) {
|
||||
cell.textLabel.text = @"Ankunftszeit teilen";
|
||||
cell.textLabel.textColor = [BVGTheme link];
|
||||
cell.textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
return cell;
|
||||
}
|
||||
cell.textLabel.text = @"Navigation beenden";
|
||||
cell.textLabel.textColor = [BVGTheme red];
|
||||
cell.textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
BOOL share = indexPath.row == 0;
|
||||
cell.titleLabel.text = share ? @"Ankunftszeit teilen" : @"Navigation beenden";
|
||||
cell.titleLabel.font = [BVGTheme font:15 weight:UIFontWeightRegular];
|
||||
cell.titleLabel.textColor = share ? [BVGTheme link] : [BVGTheme red];
|
||||
cell.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
return cell;
|
||||
}
|
||||
@@ -267,6 +269,14 @@ typedef NS_ENUM(NSInteger, GSection) { GSectionStatus, GSectionAlternative, GSec
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [[self cellAt:indexPath] fittingHeightForTableWidth:tableView.bounds.size.width];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [self cellAt:indexPath];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
GuidanceManager *g = [GuidanceManager shared];
|
||||
|
||||
@@ -46,14 +46,14 @@
|
||||
CGFloat w = self.view.bounds.size.width;
|
||||
UIView *footer = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 96)];
|
||||
UIButton *start = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
start.frame = CGRectMake([BVGTheme cardInset], 20, w - 2 * [BVGTheme cardInset], 54);
|
||||
start.frame = CGRectMake([BVGTheme cardInset], 16, w - 2 * [BVGTheme cardInset], 46);
|
||||
start.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
start.backgroundColor = [BVGTheme accent];
|
||||
start.layer.cornerRadius = 14;
|
||||
start.titleLabel.font = [BVGTheme font:18 weight:UIFontWeightSemibold];
|
||||
start.backgroundColor = [BVGTheme textPrimary];
|
||||
start.layer.cornerRadius = [BVGTheme cardRadius];
|
||||
start.titleLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
[start setTitle:@"Navigation starten" forState:UIControlStateNormal];
|
||||
[start setTitleColor:[BVGTheme textPrimary] forState:UIControlStateNormal];
|
||||
[start setTitleColor:[BVGTheme textSecondary] forState:UIControlStateHighlighted];
|
||||
[start setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
[start setTitleColor:[BVGTheme textTertiary] forState:UIControlStateHighlighted];
|
||||
[start addTarget:self action:@selector(startGuidance) forControlEvents:UIControlEventTouchUpInside];
|
||||
[footer addSubview:start];
|
||||
self.tableView.tableFooterView = footer;
|
||||
@@ -128,7 +128,7 @@
|
||||
NSNumber *dist = NN(leg[@"distance"]);
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"🚶 %ld min zu Fuß%@", (long)mins,
|
||||
dist ? [NSString stringWithFormat:@" · %@ m", dist] : @""];
|
||||
cell.textLabel.font = [BVGTheme font:15 weight:UIFontWeightMedium];
|
||||
cell.textLabel.font = [BVGTheme font:14 weight:UIFontWeightMedium];
|
||||
cell.textLabel.textColor = [BVGTheme textSecondary];
|
||||
cell.cardColor = [BVGTheme background];
|
||||
return cell;
|
||||
@@ -136,8 +136,8 @@
|
||||
|
||||
if (indexPath.row == 0) {
|
||||
cell.imageView.image = [BVGAPI badgeForLine:leg[@"line"]];
|
||||
cell.textLabel.text = NN(leg[@"direction"]) ?: @"";
|
||||
cell.textLabel.font = [BVGTheme font:17 weight:UIFontWeightSemibold];
|
||||
cell.textLabel.text = [@"→ " stringByAppendingString:[BVGAPI shortName:NN(leg[@"direction"])] ?: @""];
|
||||
cell.textLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
cell.detailTextLabel.text = [NN(leg[@"cancelled"]) boolValue] ? @"Fällt aus" : @"Fahrtverlauf ansehen";
|
||||
cell.detailTextLabel.textColor = [NN(leg[@"cancelled"]) boolValue] ? [BVGTheme red] : [BVGTheme link];
|
||||
cell.detailTextLabel.font = [BVGTheme font:13 weight:UIFontWeightMedium];
|
||||
@@ -152,13 +152,13 @@
|
||||
NSDictionary *place = NN(leg[isDep ? @"origin" : @"destination"]);
|
||||
NSDate *t = isDep ? [JourneyDetailViewController dep:leg] : [JourneyDetailViewController arr:leg];
|
||||
NSInteger delay = [NN(leg[isDep ? @"departureDelay" : @"arrivalDelay"]) integerValue] / 60;
|
||||
NSString *platform = NN(leg[isDep ? @"departurePlatform" : @"arrivalPlatform"]);
|
||||
NSString *platform = [BVGAPI platform:NN(leg[isDep ? @"departurePlatform" : @"arrivalPlatform"])];
|
||||
|
||||
NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:[BVGAPI hhmm:t]
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme digits:17 weight:UIFontWeightSemibold],
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme digits:15 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: delay > 0 ? [BVGTheme red] : [BVGTheme textPrimary] }];
|
||||
[title appendAttributedString:[[NSAttributedString alloc] initWithString:[@" " stringByAppendingString:NN(place[@"name"]) ?: @"?"]
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:16 weight:UIFontWeightRegular],
|
||||
[title appendAttributedString:[[NSAttributedString alloc] initWithString:[@" " stringByAppendingString:[BVGAPI shortName:NN(place[@"name"])] ?: @"?"]
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:15 weight:UIFontWeightRegular],
|
||||
NSForegroundColorAttributeName: [BVGTheme textPrimary] }]];
|
||||
cell.textLabel.attributedText = title;
|
||||
|
||||
|
||||
+269
-210
@@ -23,12 +23,14 @@
|
||||
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
_timeLabel = [[UILabel alloc] init];
|
||||
_timeLabel.font = [BVGTheme digits:20 weight:UIFontWeightSemibold];
|
||||
_timeLabel.font = [BVGTheme digits:17 weight:UIFontWeightSemibold];
|
||||
_timeLabel.textColor = [BVGTheme textPrimary];
|
||||
_timeLabel.adjustsFontSizeToFitWidth = YES;
|
||||
_timeLabel.minimumScaleFactor = 0.8;
|
||||
[self.contentView addSubview:_timeLabel];
|
||||
|
||||
_durationLabel = [[UILabel alloc] init];
|
||||
_durationLabel.font = [BVGTheme digits:15 weight:UIFontWeightSemibold];
|
||||
_durationLabel.font = [BVGTheme digits:14 weight:UIFontWeightMedium];
|
||||
_durationLabel.textColor = [BVGTheme textSecondary];
|
||||
_durationLabel.textAlignment = NSTextAlignmentRight;
|
||||
[self.contentView addSubview:_durationLabel];
|
||||
@@ -38,7 +40,7 @@
|
||||
[self.contentView addSubview:_linesView];
|
||||
|
||||
_infoLabel = [[UILabel alloc] init];
|
||||
_infoLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
_infoLabel.font = [BVGTheme font:12 weight:UIFontWeightRegular];
|
||||
_infoLabel.textColor = [BVGTheme textSecondary];
|
||||
[self.contentView addSubview:_infoLabel];
|
||||
|
||||
@@ -50,18 +52,25 @@
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
CGFloat w = self.contentView.bounds.size.width;
|
||||
self.timeLabel.frame = CGRectMake(16, 10, w - 110, 24);
|
||||
self.durationLabel.frame = CGRectMake(w - 96, 10, 84, 24);
|
||||
self.linesView.frame = CGRectMake(16, 38, w - 28, 24);
|
||||
self.infoLabel.frame = CGRectMake(16, 66, w - 28, 17);
|
||||
self.timeLabel.frame = CGRectMake(14, 9, w - 110, 21);
|
||||
self.durationLabel.frame = CGRectMake(w - 92, 10, 84, 19);
|
||||
self.linesView.frame = CGRectMake(14, 34, w - 22, 22);
|
||||
self.infoLabel.frame = CGRectMake(14, 60, w - 22, 15);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Routenplaner
|
||||
|
||||
typedef NS_ENUM(NSInteger, JourneySection) {
|
||||
JourneySectionDate, // Datumsauswahl (nur bei "Abfahrt um" / "Ankunft um")
|
||||
JourneySectionSearch, // Suchen-Knopf + Statuszeile
|
||||
JourneySectionQuick, // Schnellziele (wenn noch keine Ergebnisse)
|
||||
JourneySectionResult, // je eine Karte pro Verbindung
|
||||
};
|
||||
|
||||
@interface JourneyViewController () <CLLocationManagerDelegate>
|
||||
@property (nonatomic, strong) NSDictionary *from; // nil = Mein Standort, sonst {id, name}
|
||||
@property (nonatomic, strong) NSDictionary *from; // nil = Mein Standort, sonst {id, name} oder {name, latitude, longitude}
|
||||
@property (nonatomic, strong) NSDictionary *to;
|
||||
@property (nonatomic, strong) ChipBar *timeControl; // Jetzt / Abfahrt um / Ankunft um
|
||||
@property (nonatomic, strong) UIDatePicker *datePicker;
|
||||
@@ -79,13 +88,11 @@
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.largeTitle = @"Route";
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"Umstiege mit mind. %ld min Puffer", (long)[BVGSettings transferMinutes]];
|
||||
self.largeTitle = @"Verbindung";
|
||||
self.journeys = @[];
|
||||
|
||||
__weak JourneyViewController *weakSelf = self;
|
||||
self.timeControl = [[ChipBar alloc] initWithItems:@[@"Jetzt", @"Abfahrt um", @"Ankunft um"]];
|
||||
self.timeControl.idleColor = [BVGTheme background];
|
||||
self.timeControl.onChange = ^(NSInteger idx) { [weakSelf timeModeChanged]; };
|
||||
|
||||
self.datePicker = [[UIDatePicker alloc] init];
|
||||
@@ -94,102 +101,108 @@
|
||||
self.datePicker.minuteInterval = 1;
|
||||
self.datePicker.date = [NSDate dateWithTimeIntervalSinceNow:15 * 60];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateNaviButton)
|
||||
name:GuidanceChangedNotification object:nil];
|
||||
[self buildHeader];
|
||||
|
||||
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
||||
[nc addObserver:self selector:@selector(updateNaviButton) name:GuidanceChangedNotification object:nil];
|
||||
[nc addObserver:self selector:@selector(settingsChanged) name:BVGSettingsChangedNotification object:nil];
|
||||
[self updateNaviButton];
|
||||
[self buildQuickPlaces];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(settingsChanged)
|
||||
name:BVGSettingsChangedNotification object:nil];
|
||||
|
||||
self.locationManager = [[CLLocationManager alloc] init];
|
||||
self.locationManager.delegate = self;
|
||||
self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
|
||||
|
||||
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"⇅"
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(swap)];
|
||||
}
|
||||
|
||||
#pragma mark - Schnellziele
|
||||
|
||||
- (void)settingsChanged {
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"Umstiege mit mind. %ld min Puffer", (long)[BVGSettings transferMinutes]];
|
||||
[self buildQuickPlaces];
|
||||
}
|
||||
|
||||
- (void)buildQuickPlaces {
|
||||
CGFloat w = self.view.bounds.size.width;
|
||||
CGFloat inset = [BVGTheme cardInset];
|
||||
CGFloat bw = (w - 2 * inset - 10) / 2;
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 74)];
|
||||
|
||||
NSArray *defs = @[@[@"home", @"🏠", @"Nach Hause"], @[@"work", @"💼", @"Zur Arbeit"]];
|
||||
for (NSUInteger i = 0; i < defs.count; i++) {
|
||||
NSDictionary *place = [BVGSettings placeForKey:defs[i][0]];
|
||||
UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
b.frame = CGRectMake(inset + i * (bw + 10), 4, bw, 64);
|
||||
b.autoresizingMask = i == 0 ? UIViewAutoresizingFlexibleRightMargin : UIViewAutoresizingFlexibleLeftMargin;
|
||||
b.backgroundColor = [BVGTheme card];
|
||||
b.layer.cornerRadius = [BVGTheme cardRadius];
|
||||
b.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
b.contentEdgeInsets = UIEdgeInsetsMake(0, 14, 0, 10);
|
||||
b.titleLabel.numberOfLines = 2;
|
||||
b.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
b.tag = i;
|
||||
|
||||
NSMutableAttributedString *t = [[NSMutableAttributedString alloc]
|
||||
initWithString:[NSString stringWithFormat:@"%@ %@\n", defs[i][1], defs[i][2]]
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:16 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: [BVGTheme textPrimary] }];
|
||||
[t appendAttributedString:[[NSAttributedString alloc] initWithString:(place ? place[@"name"] : @"Festlegen …")
|
||||
attributes:@{ NSFontAttributeName: [BVGTheme font:13 weight:UIFontWeightRegular],
|
||||
NSForegroundColorAttributeName: place ? [BVGTheme textSecondary] : [BVGTheme link] }]];
|
||||
[b setAttributedTitle:t forState:UIControlStateNormal];
|
||||
[b addTarget:self action:@selector(quickPlaceTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[b addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(quickPlaceLongPress:)]];
|
||||
b.accessibilityHint = @"Gedrückt halten zum Ändern";
|
||||
[v addSubview:b];
|
||||
}
|
||||
self.headerAccessory = v;
|
||||
}
|
||||
|
||||
- (NSString *)quickKey:(NSInteger)idx {
|
||||
return idx == 0 ? @"home" : @"work";
|
||||
}
|
||||
|
||||
- (void)quickPlaceTapped:(UIButton *)b {
|
||||
NSDictionary *place = [BVGSettings placeForKey:[self quickKey:b.tag]];
|
||||
if (!place) { [self editQuickPlace:b.tag]; return; }
|
||||
self.from = nil; // ab meinem Standort
|
||||
self.to = place;
|
||||
self.timeControl.selectedIndex = 0;
|
||||
[self.tableView reloadData];
|
||||
[self startSearch];
|
||||
}
|
||||
|
||||
- (void)quickPlaceLongPress:(UILongPressGestureRecognizer *)g {
|
||||
if (g.state == UIGestureRecognizerStateBegan) [self editQuickPlace:g.view.tag];
|
||||
}
|
||||
|
||||
- (void)editQuickPlace:(NSInteger)idx {
|
||||
NSString *key = [self quickKey:idx];
|
||||
__weak JourneyViewController *weakSelf = self;
|
||||
SearchViewController *search = [[SearchViewController alloc] initWithStyle:UITableViewStylePlain];
|
||||
search.allowAddresses = YES;
|
||||
search.pickHandler = ^(NSDictionary *place) {
|
||||
[BVGSettings setPlace:place forKey:key];
|
||||
[weakSelf buildQuickPlaces];
|
||||
};
|
||||
[self.navigationController pushViewController:search animated:YES];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)settingsChanged {
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - Kopfbereich: Von / Nach + Zeit
|
||||
|
||||
- (UIButton *)endpointButton:(BOOL)isFrom frame:(CGRect)frame {
|
||||
UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
b.frame = frame;
|
||||
b.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
b.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
b.contentEdgeInsets = UIEdgeInsetsMake(0, 12, 0, 8);
|
||||
[b setBackgroundImage:[BVGTheme imageWithColor:[BVGTheme cardPressed]] forState:UIControlStateHighlighted];
|
||||
b.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
|
||||
NSString *label = isFrom ? @"Von " : @"Nach ";
|
||||
NSString *value;
|
||||
BOOL placeholder = NO;
|
||||
if (isFrom) value = self.from ? [BVGAPI shortName:self.from[@"name"]] : @"Mein Standort";
|
||||
else if (self.to) value = [BVGAPI shortName:self.to[@"name"]];
|
||||
else { value = @"Ziel eingeben"; placeholder = YES; }
|
||||
|
||||
NSMutableAttributedString *t = [[NSMutableAttributedString alloc] initWithString:label attributes:@{
|
||||
NSFontAttributeName: [BVGTheme font:13 weight:UIFontWeightRegular],
|
||||
NSForegroundColorAttributeName: [BVGTheme textSecondary] }];
|
||||
[t appendAttributedString:[[NSAttributedString alloc] initWithString:value ?: @"" attributes:@{
|
||||
NSFontAttributeName: [BVGTheme font:15 weight:(placeholder ? UIFontWeightRegular : UIFontWeightSemibold)],
|
||||
NSForegroundColorAttributeName: placeholder ? [BVGTheme textTertiary] : [BVGTheme textPrimary] }]];
|
||||
[b setAttributedTitle:t forState:UIControlStateNormal];
|
||||
b.accessibilityLabel = [NSString stringWithFormat:@"%@ %@", isFrom ? @"Start" : @"Ziel", value];
|
||||
b.tag = isFrom ? 0 : 1;
|
||||
[b addTarget:self action:@selector(endpointTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
return b;
|
||||
}
|
||||
|
||||
- (void)buildHeader {
|
||||
CGFloat w = self.view.bounds.size.width;
|
||||
CGFloat inset = [BVGTheme cardInset];
|
||||
CGFloat cardW = w - 2 * inset;
|
||||
CGFloat rowH = 42;
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 2 * rowH + 46)];
|
||||
|
||||
// weisse Karte mit zwei Zeilen und Tausch-Knopf rechts
|
||||
UIView *card = [[UIView alloc] initWithFrame:CGRectMake(inset, 0, cardW, 2 * rowH)];
|
||||
card.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
card.backgroundColor = [UIColor whiteColor];
|
||||
card.layer.cornerRadius = [BVGTheme cardRadius];
|
||||
card.clipsToBounds = YES;
|
||||
[v addSubview:card];
|
||||
|
||||
[card addSubview:[self endpointButton:YES frame:CGRectMake(0, 0, cardW - 46, rowH)]];
|
||||
[card addSubview:[self endpointButton:NO frame:CGRectMake(0, rowH, cardW - 46, rowH)]];
|
||||
|
||||
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(12, rowH, cardW - 58, 1.0 / [UIScreen mainScreen].scale)];
|
||||
line.backgroundColor = [BVGTheme separator];
|
||||
line.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[card addSubview:line];
|
||||
|
||||
UIButton *swap = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
swap.frame = CGRectMake(cardW - 42, rowH - 16, 32, 32);
|
||||
swap.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
|
||||
[swap setBackgroundImage:[BVGTheme roundedImageWithColor:[BVGTheme background] radius:16] forState:UIControlStateNormal];
|
||||
[swap setBackgroundImage:[BVGTheme roundedImageWithColor:[BVGTheme cardPressed] radius:16] forState:UIControlStateHighlighted];
|
||||
swap.titleLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
[swap setTitle:@"⇅" forState:UIControlStateNormal];
|
||||
[swap setTitleColor:[BVGTheme textPrimary] forState:UIControlStateNormal];
|
||||
swap.accessibilityLabel = @"Start und Ziel tauschen";
|
||||
[swap addTarget:self action:@selector(swap) forControlEvents:UIControlEventTouchUpInside];
|
||||
[card addSubview:swap];
|
||||
|
||||
// Zeit-Chips darunter
|
||||
[self.timeControl removeFromSuperview];
|
||||
self.timeControl.frame = CGRectMake(0, 2 * rowH + 2, w, 44);
|
||||
self.timeControl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[v addSubview:self.timeControl];
|
||||
|
||||
self.headerAccessory = v;
|
||||
}
|
||||
|
||||
- (void)endpointTapped:(UIButton *)b {
|
||||
[self chooseEndpoint:(b.tag == 0)];
|
||||
}
|
||||
|
||||
- (void)updateNaviButton {
|
||||
self.navigationItem.rightBarButtonItem = [GuidanceManager shared].active
|
||||
? [[UIBarButtonItem alloc] initWithTitle:@"Navigation" style:UIBarButtonItemStyleDone target:self action:@selector(openGuidance)]
|
||||
? [[UIBarButtonItem alloc] initWithTitle:@"Navigation ›" style:UIBarButtonItemStyleDone target:self action:@selector(openGuidance)]
|
||||
: nil;
|
||||
}
|
||||
|
||||
@@ -202,62 +215,81 @@
|
||||
if (self.timeControl.selectedIndex > 0 && [self.datePicker.date timeIntervalSinceNow] < -3600) {
|
||||
self.datePicker.date = [NSDate dateWithTimeIntervalSinceNow:15 * 60];
|
||||
}
|
||||
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)swap {
|
||||
if (!self.to) return;
|
||||
NSDictionary *oldFrom = self.from;
|
||||
self.from = self.to;
|
||||
self.to = oldFrom; // nil (Mein Standort) als Ziel ist nicht erlaubt -> unten abgefangen
|
||||
self.to = oldFrom; // nil (Mein Standort) als Ziel ist nicht erlaubt
|
||||
if (!self.to) {
|
||||
self.to = self.from;
|
||||
self.from = nil;
|
||||
}
|
||||
self.journeys = @[];
|
||||
self.errorText = nil;
|
||||
[self buildHeader];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - Auswahl
|
||||
#pragma mark - Auswahl von Start/Ziel: direkt die Suche mit Schnellauswahl
|
||||
|
||||
- (void)chooseEndpoint:(BOOL)isFrom {
|
||||
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:(isFrom ? @"Start" : @"Ziel")
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
__weak JourneyViewController *weakSelf = self;
|
||||
if (isFrom) {
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:@"📍 Mein Standort" style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *a) { weakSelf.from = nil; [weakSelf.tableView reloadData]; }]];
|
||||
}
|
||||
for (NSArray *pl in @[@[@"home", @"🏠 "], @[@"work", @"💼 "]]) {
|
||||
NSDictionary *place = [BVGSettings placeForKey:pl[0]];
|
||||
if (!place) continue;
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:[pl[1] stringByAppendingString:place[@"name"]] style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *a) { [weakSelf setEndpoint:place isFrom:isFrom]; }]];
|
||||
}
|
||||
NSInteger n = 0;
|
||||
for (NSDictionary *f in [BVGAPI favorites]) {
|
||||
if (n++ >= 4) break;
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:[@"★ " stringByAppendingString:f[@"name"]] style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *a) { [weakSelf setEndpoint:f isFrom:isFrom]; }]];
|
||||
}
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:@"🔍 Haltestelle oder Adresse suchen …" style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *a) {
|
||||
SearchViewController *search = [[SearchViewController alloc] initWithStyle:UITableViewStylePlain];
|
||||
search.allowAddresses = YES;
|
||||
search.pickHandler = ^(NSDictionary *stop) { [weakSelf setEndpoint:stop isFrom:isFrom]; };
|
||||
[weakSelf.navigationController pushViewController:search animated:YES];
|
||||
}]];
|
||||
[sheet addAction:[UIAlertAction actionWithTitle:@"Abbrechen" style:UIAlertActionStyleCancel handler:nil]];
|
||||
sheet.popoverPresentationController.sourceView = self.view;
|
||||
[self presentViewController:sheet animated:YES completion:nil];
|
||||
SearchViewController *search = [[SearchViewController alloc] initWithStyle:UITableViewStylePlain];
|
||||
search.allowAddresses = YES;
|
||||
search.offerSavedPlaces = YES;
|
||||
search.offerCurrentLocation = isFrom;
|
||||
search.pickTitle = isFrom ? @"Start" : @"Ziel";
|
||||
search.pickHandler = ^(NSDictionary *place) {
|
||||
if (!isFrom && !place) return; // "Mein Standort" nur als Start
|
||||
[weakSelf setEndpoint:place isFrom:isFrom];
|
||||
};
|
||||
[self.navigationController pushViewController:search animated:YES];
|
||||
}
|
||||
|
||||
- (void)setEndpoint:(NSDictionary *)stop isFrom:(BOOL)isFrom {
|
||||
if (isFrom) self.from = stop; else self.to = stop;
|
||||
- (void)setEndpoint:(NSDictionary *)place isFrom:(BOOL)isFrom {
|
||||
if (isFrom) self.from = place; else self.to = place;
|
||||
self.journeys = @[];
|
||||
self.errorText = nil;
|
||||
[self buildHeader];
|
||||
[self.tableView reloadData];
|
||||
if (self.to) [self startSearch]; // Ziel steht -> gleich suchen (wie in der BVG-App)
|
||||
}
|
||||
|
||||
#pragma mark - Schnellziele
|
||||
|
||||
- (NSString *)quickKey:(NSInteger)idx {
|
||||
return idx == 0 ? @"home" : @"work";
|
||||
}
|
||||
|
||||
- (void)quickPlaceTapped:(NSInteger)idx {
|
||||
NSDictionary *place = [BVGSettings placeForKey:[self quickKey:idx]];
|
||||
if (!place) { [self editQuickPlace:idx]; return; }
|
||||
self.from = nil; // ab meinem Standort
|
||||
self.timeControl.selectedIndex = 0;
|
||||
[self setEndpoint:place isFrom:NO];
|
||||
}
|
||||
|
||||
- (void)editQuickPlace:(NSInteger)idx {
|
||||
NSString *key = [self quickKey:idx];
|
||||
__weak JourneyViewController *weakSelf = self;
|
||||
SearchViewController *search = [[SearchViewController alloc] initWithStyle:UITableViewStylePlain];
|
||||
search.allowAddresses = YES;
|
||||
search.pickTitle = idx == 0 ? @"Zuhause festlegen" : @"Arbeit festlegen";
|
||||
search.pickHandler = ^(NSDictionary *place) {
|
||||
if (!place) return;
|
||||
[BVGSettings setPlace:place forKey:key];
|
||||
[weakSelf.tableView reloadData];
|
||||
};
|
||||
[self.navigationController pushViewController:search animated:YES];
|
||||
}
|
||||
|
||||
- (void)quickLongPress:(UILongPressGestureRecognizer *)g {
|
||||
if (g.state != UIGestureRecognizerStateBegan) return;
|
||||
NSIndexPath *ip = [self.tableView indexPathForRowAtPoint:[g locationInView:self.tableView]];
|
||||
if (ip && [self kindOfSection:ip.section] == JourneySectionQuick) [self editQuickPlace:ip.row];
|
||||
}
|
||||
|
||||
#pragma mark - Suche
|
||||
@@ -298,7 +330,7 @@
|
||||
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
|
||||
if (!self.searching) return;
|
||||
self.searching = NO;
|
||||
self.errorText = @"Standort nicht verfügbar. Wähle eine Start-Haltestelle.";
|
||||
self.errorText = @"Standort nicht verfügbar. Tippe auf „Von“ und wähle eine Haltestelle.";
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
@@ -321,7 +353,7 @@
|
||||
NSLog(@"Journey-Fehler: %@ %@", error, json);
|
||||
self.errorText = @"Keine Verbindung gefunden oder Server nicht erreichbar.";
|
||||
}
|
||||
// Sicherheitsnetz: nur Verbindungen mit mind. 5 min pro Umstieg zeigen
|
||||
// Sicherheitsnetz: nur Verbindungen mit mind. X min pro Umstieg zeigen
|
||||
NSMutableArray *ok = [NSMutableArray array];
|
||||
for (NSDictionary *j in list ?: @[]) {
|
||||
id legs = NN(j[@"legs"]);
|
||||
@@ -336,20 +368,12 @@
|
||||
|
||||
#pragma mark - Auswertung einer Verbindung
|
||||
|
||||
+ (NSDate *)legDeparture:(NSDictionary *)leg {
|
||||
return [BVGAPI dateFromISO:leg[@"departure"]] ?: [BVGAPI dateFromISO:leg[@"plannedDeparture"]];
|
||||
}
|
||||
|
||||
+ (NSDate *)legArrival:(NSDictionary *)leg {
|
||||
return [BVGAPI dateFromISO:leg[@"arrival"]] ?: [BVGAPI dateFromISO:leg[@"plannedArrival"]];
|
||||
}
|
||||
|
||||
- (void)configureCell:(JourneyCell *)cell journey:(NSDictionary *)j {
|
||||
NSArray *legs = NN(j[@"legs"]);
|
||||
if (![legs isKindOfClass:[NSArray class]] || !legs.count) return;
|
||||
|
||||
NSDate *dep = [JourneyViewController legDeparture:legs.firstObject];
|
||||
NSDate *arr = [JourneyViewController legArrival:legs.lastObject];
|
||||
NSDate *dep = [GuidanceManager legDeparture:legs.firstObject];
|
||||
NSDate *arr = [GuidanceManager legArrival:legs.lastObject];
|
||||
NSInteger mins = (dep && arr) ? (NSInteger)round([arr timeIntervalSinceDate:dep] / 60.0) : 0;
|
||||
|
||||
NSMutableArray *lines = [NSMutableArray array];
|
||||
@@ -363,130 +387,165 @@
|
||||
[lines addObject:leg[@"line"]];
|
||||
if (firstDelay < 0) {
|
||||
firstDelay = [NN(leg[@"departureDelay"]) integerValue] / 60;
|
||||
firstStop = NN(NN(leg[@"origin"])[@"name"]);
|
||||
firstTransitDep = [JourneyViewController legDeparture:leg];
|
||||
firstStop = [BVGAPI shortName:NN(NN(leg[@"origin"])[@"name"])];
|
||||
firstTransitDep = [GuidanceManager legDeparture:leg];
|
||||
}
|
||||
}
|
||||
|
||||
cell.timeLabel.text = [NSString stringWithFormat:@"%@ → %@", [BVGAPI hhmm:dep], [BVGAPI hhmm:arr]];
|
||||
cell.durationLabel.text = [NSString stringWithFormat:@"%ld min", (long)mins];
|
||||
cell.timeLabel.text = [NSString stringWithFormat:@"%@ – %@", [BVGAPI hhmm:dep], [BVGAPI hhmm:arr]];
|
||||
cell.durationLabel.text = mins >= 60 ? [NSString stringWithFormat:@"%ld:%02ld h", (long)(mins / 60), (long)(mins % 60)]
|
||||
: [NSString stringWithFormat:@"%ld min", (long)mins];
|
||||
cell.linesView.image = [BVGAPI badgeStripForLines:lines];
|
||||
|
||||
NSMutableArray *info = [NSMutableArray array];
|
||||
NSInteger transfers = MAX((NSInteger)lines.count - 1, 0);
|
||||
if (transfers == 0) {
|
||||
[info addObject:@"direkt"];
|
||||
} else {
|
||||
NSTimeInterval slack = [GuidanceManager minTransferSlack:legs];
|
||||
[info addObject:[NSString stringWithFormat:@"%ld× umsteigen (mind. %ld min)", (long)transfers, (long)floor(slack / 60.0)]];
|
||||
}
|
||||
if (transfers == 0) [info addObject:@"direkt"];
|
||||
else [info addObject:[NSString stringWithFormat:@"%ld× umsteigen", (long)transfers]];
|
||||
if (firstStop && firstTransitDep) {
|
||||
NSInteger leave = (NSInteger)floor([firstTransitDep timeIntervalSinceNow] / 60.0);
|
||||
[info addObject:leave > 0 ? [NSString stringWithFormat:@"in %ld min ab %@", (long)leave, firstStop]
|
||||
: [NSString stringWithFormat:@"ab %@", firstStop]];
|
||||
}
|
||||
if (firstDelay > 0) [info addObject:[NSString stringWithFormat:@"+%ld", (long)firstDelay]];
|
||||
if (cancelled) [info addObject:@"⚠ Ausfall"];
|
||||
UIColor *color = [BVGTheme textSecondary];
|
||||
if (firstDelay > 0) { [info insertObject:[NSString stringWithFormat:@"+%ld min", (long)firstDelay] atIndex:0]; color = [BVGTheme red]; }
|
||||
if (cancelled) { [info insertObject:@"⚠︎ Ausfall" atIndex:0]; color = [BVGTheme red]; }
|
||||
|
||||
cell.infoLabel.text = [info componentsJoinedByString:@" · "];
|
||||
cell.infoLabel.textColor = (cancelled || firstDelay > 0) ? [BVGTheme red] : [BVGTheme textSecondary];
|
||||
cell.infoLabel.textColor = color;
|
||||
cell.accessibilityLabel = [NSString stringWithFormat:@"%@ bis %@, %ld Minuten, %@",
|
||||
[BVGAPI hhmm:dep], [BVGAPI hhmm:arr], (long)mins, cell.infoLabel.text];
|
||||
}
|
||||
|
||||
#pragma mark - Table
|
||||
|
||||
// Abschnitte: 0 Von/Nach · 1 Zeit · 2 Suchen-Knopf · 3+ je eine Karte pro Verbindung
|
||||
- (NSArray *)sectionKinds {
|
||||
NSMutableArray *k = [NSMutableArray array];
|
||||
if (self.timeControl.selectedIndex > 0) [k addObject:@(JourneySectionDate)];
|
||||
[k addObject:@(JourneySectionSearch)];
|
||||
if (!self.journeys.count && !self.searching) [k addObject:@(JourneySectionQuick)];
|
||||
for (NSUInteger i = 0; i < self.journeys.count; i++) [k addObject:@(JourneySectionResult)];
|
||||
return k;
|
||||
}
|
||||
|
||||
- (JourneySection)kindOfSection:(NSInteger)section {
|
||||
NSArray *k = [self sectionKinds];
|
||||
return section < (NSInteger)k.count ? [k[section] integerValue] : JourneySectionResult;
|
||||
}
|
||||
|
||||
- (NSDictionary *)journeyForSection:(NSInteger)section {
|
||||
NSInteger first = [[self sectionKinds] indexOfObject:@(JourneySectionResult)];
|
||||
NSInteger i = section - first;
|
||||
return (first != NSNotFound && i >= 0 && i < (NSInteger)self.journeys.count) ? self.journeys[i] : nil;
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 3 + self.journeys.count;
|
||||
return [self sectionKinds].count;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
if (section == 1) return @"Wann?";
|
||||
if (section == 3) return @"Verbindungen";
|
||||
JourneySection kind = [self kindOfSection:section];
|
||||
if (kind == JourneySectionQuick) return @"Schnellziele";
|
||||
if (kind == JourneySectionResult && section > 0 && [self kindOfSection:section - 1] != JourneySectionResult) {
|
||||
return @"Verbindungen";
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
|
||||
if (section != 2) return nil;
|
||||
if (self.searching) return @"Suche Verbindungen …";
|
||||
return self.errorText;
|
||||
JourneySection kind = [self kindOfSection:section];
|
||||
if (kind == JourneySectionSearch) {
|
||||
if (self.searching) return @"Suche Verbindungen …";
|
||||
if (self.errorText) return self.errorText;
|
||||
return [NSString stringWithFormat:@"Umstiege immer mit mind. %ld min Puffer.", (long)[BVGSettings transferMinutes]];
|
||||
}
|
||||
if (kind == JourneySectionQuick) return @"Gedrückt halten zum Ändern.";
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
switch (section) {
|
||||
case 0: return 2;
|
||||
case 1: return self.timeControl.selectedIndex == 0 ? 1 : 2;
|
||||
default: return 1;
|
||||
}
|
||||
return [self kindOfSection:section] == JourneySectionQuick ? 2 : 1;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.section == 0) return 60;
|
||||
if (indexPath.section == 1) return indexPath.row == 1 ? 180 : 56;
|
||||
if (indexPath.section == 2) return 52;
|
||||
return 94;
|
||||
}
|
||||
|
||||
// kleine runde Markierung: Start gruen gefuellt, Ziel als Ring
|
||||
+ (UIImage *)endpointIcon:(BOOL)isFrom {
|
||||
CGSize size = CGSizeMake(22, 22);
|
||||
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
||||
UIBezierPath *p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(4, 4, 14, 14)];
|
||||
if (isFrom) { [[BVGTheme green] setFill]; [p fill]; }
|
||||
else { p.lineWidth = 4; [[BVGTheme red] setStroke]; [p stroke]; }
|
||||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return img;
|
||||
switch ([self kindOfSection:indexPath.section]) {
|
||||
case JourneySectionDate: return 180;
|
||||
case JourneySectionSearch: return 44;
|
||||
case JourneySectionQuick: return 50;
|
||||
case JourneySectionResult: return 84;
|
||||
}
|
||||
return 52;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.section >= 3) {
|
||||
JourneySection kind = [self kindOfSection:indexPath.section];
|
||||
|
||||
if (kind == JourneySectionResult) {
|
||||
JourneyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"journey"];
|
||||
if (!cell) cell = [[JourneyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"journey"];
|
||||
[self configureCell:cell journey:self.journeys[indexPath.section - 3]];
|
||||
[self configureCell:cell journey:[self journeyForSection:indexPath.section]];
|
||||
return cell;
|
||||
}
|
||||
|
||||
CardCell *cell = [[CardCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil];
|
||||
if (indexPath.section == 0) {
|
||||
BOOL isFrom = indexPath.row == 0;
|
||||
cell.imageView.image = [JourneyViewController endpointIcon:isFrom];
|
||||
cell.textLabel.font = [BVGTheme font:17 weight:UIFontWeightSemibold];
|
||||
if (isFrom) cell.textLabel.text = self.from ? self.from[@"name"] : @"Mein Standort";
|
||||
else cell.textLabel.text = self.to ? self.to[@"name"] : @"Ziel wählen";
|
||||
if (!isFrom && !self.to) cell.textLabel.textColor = [BVGTheme link];
|
||||
cell.detailTextLabel.text = isFrom ? @"Start" : @"Ziel";
|
||||
cell.detailTextLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
} else if (indexPath.section == 1 && indexPath.row == 1) {
|
||||
if (kind == JourneySectionDate) {
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
[self.datePicker removeFromSuperview];
|
||||
self.datePicker.frame = CGRectMake(0, 0, tableView.bounds.size.width - 2 * [BVGTheme cardInset], 180);
|
||||
self.datePicker.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[cell.contentView addSubview:self.datePicker];
|
||||
} else if (indexPath.section == 1) {
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
[self.timeControl removeFromSuperview];
|
||||
self.timeControl.frame = CGRectMake(-[BVGTheme cardInset] + 12, 6, tableView.bounds.size.width - 2 * [BVGTheme cardInset], 44);
|
||||
[cell.contentView addSubview:self.timeControl];
|
||||
} else {
|
||||
} else if (kind == JourneySectionSearch) {
|
||||
// dunkler Hauptknopf
|
||||
cell.textLabel.text = self.searching ? @"Suche …" : @"Verbindungen suchen";
|
||||
cell.textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cell.textLabel.font = [BVGTheme font:17 weight:UIFontWeightSemibold];
|
||||
cell.textLabel.textColor = [BVGTheme textPrimary];
|
||||
cell.cardColor = [BVGTheme accent];
|
||||
cell.textLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
cell.textLabel.textColor = [UIColor whiteColor];
|
||||
cell.cardColor = [BVGTheme textPrimary];
|
||||
cell.accessibilityTraits = UIAccessibilityTraitButton;
|
||||
} else {
|
||||
NSArray *defs = @[@[@"home", @"⌂", @"Nach Hause"], @[@"work", @"✱", @"Zur Arbeit"]];
|
||||
NSArray *d = defs[indexPath.row];
|
||||
NSDictionary *place = [BVGSettings placeForKey:d[0]];
|
||||
cell.imageView.image = [JourneyViewController roundIcon:d[1]];
|
||||
cell.textLabel.text = d[2];
|
||||
cell.textLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
cell.detailTextLabel.font = [BVGTheme font:12 weight:UIFontWeightRegular];
|
||||
cell.detailTextLabel.text = place ? place[@"name"] : @"Antippen zum Festlegen";
|
||||
cell.detailTextLabel.textColor = place ? [BVGTheme textSecondary] : [BVGTheme link];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
if (!cell.gestureRecognizers.count) {
|
||||
[cell addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(quickLongPress:)]];
|
||||
}
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
+ (UIImage *)roundIcon:(NSString *)text {
|
||||
CGSize size = CGSizeMake(28, 28);
|
||||
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
||||
[[BVGTheme accentSoft] setFill];
|
||||
[[UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 28, 28)] fill];
|
||||
NSDictionary *attrs = @{ NSFontAttributeName: [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: [BVGTheme textPrimary] };
|
||||
CGSize t = [text sizeWithAttributes:attrs];
|
||||
[text drawAtPoint:CGPointMake((28 - t.width) / 2, (28 - t.height) / 2) withAttributes:attrs];
|
||||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return img;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
if (indexPath.section == 0) {
|
||||
[self chooseEndpoint:(indexPath.row == 0)];
|
||||
} else if (indexPath.section == 2) {
|
||||
[self startSearch];
|
||||
} else if (indexPath.section >= 3) {
|
||||
JourneyDetailViewController *vc = [[JourneyDetailViewController alloc] initWithJourney:self.journeys[indexPath.section - 3]];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
switch ([self kindOfSection:indexPath.section]) {
|
||||
case JourneySectionSearch: [self startSearch]; break;
|
||||
case JourneySectionQuick: [self quickPlaceTapped:indexPath.row]; break;
|
||||
case JourneySectionResult: {
|
||||
NSDictionary *j = [self journeyForSection:indexPath.section];
|
||||
if (!j) break;
|
||||
JourneyDetailViewController *vc = [[JourneyDetailViewController alloc] initWithJourney:j];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-16
@@ -1,20 +1,25 @@
|
||||
BVGNearby v8 – Schnellziele, Richtungsfilter, Hinweise, Teilen
|
||||
===============================================================
|
||||
BVGNearby v15 – Widget, Mitteilungs-Knoepfe, Akku, Feinschliff
|
||||
==============================================================
|
||||
|
||||
1) Alle Dateien aus dem ZIP in den Ordner BVGNearby/ kopieren (ersetzen).
|
||||
Installation (im Projektordner, neben BVGNearby.xcodeproj):
|
||||
unzip -o BVGNearby-v15.zip # ersetzt BVGNearby/, legt Widget/ an, ersetzt deploy.sh
|
||||
chmod +x deploy.sh
|
||||
./deploy.sh # macht den Methoden-Check jetzt selbst
|
||||
|
||||
2) Neue Dateien ins Xcode-Projekt aufnehmen – automatisch:
|
||||
sudo gem install xcodeproj # einmalig
|
||||
ruby BVGNearby/add_files.rb # im Ordner neben BVGNearby.xcodeproj
|
||||
Kein Xcode-Target noetig: deploy.sh baut das Widget direkt mit clang,
|
||||
legt es als PlugIns/BVGWidget.appex in die App und signiert beides mit ldid.
|
||||
Macht das Widget Aerger: NO_WIDGET=1 ./deploy.sh
|
||||
|
||||
Oder von Hand: File → Add Files … → DisruptionsViewController.h/.m
|
||||
(Target BVGNearby anhaken, "Copy items" aus).
|
||||
Widget aktivieren: Mitteilungszentrale runterziehen -> "Heute" -> ganz unten
|
||||
"Bearbeiten" -> "BVG Abfahrten" hinzufuegen.
|
||||
Zeigt die ersten 2 Favoriten (je 3 Abfahrten), ohne Favoriten die naechsten
|
||||
Haltestellen per Standort. Antippen oeffnet die Haltestelle in der App.
|
||||
|
||||
Tipp: add_files.rb kannst du auch oben in deploy.sh eintragen,
|
||||
dann passiert das bei jedem Build automatisch.
|
||||
|
||||
3) ./deploy.sh
|
||||
|
||||
Geändert: BVGSettings, SearchViewController, JourneyViewController,
|
||||
StopViewController, ViewController, SettingsViewController, GuidanceViewController
|
||||
Neu: DisruptionsViewController, add_files.rb
|
||||
Neu in v15:
|
||||
- Widget fuer die Mitteilungszentrale
|
||||
- Alternativ-Mitteilung: "Uebernehmen" / "Behalten" direkt auf dem Sperrbildschirm
|
||||
(nach links wischen) bzw. im Banner (herunterziehen)
|
||||
- Navigation spart Akku: genaues GPS nur zu Fuss und kurz vor Ein-/Aussteigen
|
||||
- Lautsprecher-Symbol statt "Ansage", Ortungspfeil in der Karte
|
||||
- Gleis "2 (U7)" -> "2"; dezenteres Badge am Verbindung-Tab
|
||||
- Karte: kleinere Haltestellen und schlankere Statusleiste
|
||||
@@ -22,7 +22,7 @@ static const CLLocationDegrees kMaxRadarSpan = 0.045; // weiter rausgezoomt ->
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.navigationItem.title = @"Live-Karte";
|
||||
self.navigationItem.title = @"Karte";
|
||||
self.vehicles = [NSMutableDictionary dictionary];
|
||||
self.stops = [NSMutableDictionary dictionary];
|
||||
|
||||
@@ -39,21 +39,20 @@ static const CLLocationDegrees kMaxRadarSpan = 0.045; // weiter rausgezoomt ->
|
||||
|
||||
// schwebende Status-Pille mit Schatten
|
||||
self.statusLabel = [[UILabel alloc] init];
|
||||
self.statusLabel.font = [BVGTheme font:13 weight:UIFontWeightSemibold];
|
||||
self.statusLabel.font = [BVGTheme font:12 weight:UIFontWeightMedium];
|
||||
self.statusLabel.textAlignment = NSTextAlignmentCenter;
|
||||
self.statusLabel.textColor = [BVGTheme textPrimary];
|
||||
self.statusLabel.backgroundColor = [UIColor whiteColor];
|
||||
self.statusLabel.layer.cornerRadius = 17;
|
||||
self.statusLabel.layer.cornerRadius = 14;
|
||||
self.statusLabel.layer.masksToBounds = NO;
|
||||
self.statusLabel.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
self.statusLabel.layer.shadowOpacity = 0.18;
|
||||
self.statusLabel.layer.shadowOpacity = 0.12;
|
||||
self.statusLabel.layer.shadowRadius = 6;
|
||||
self.statusLabel.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
[self.view addSubview:self.statusLabel];
|
||||
|
||||
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Mein Standort"
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(centerOnUser)];
|
||||
self.navigationItem.leftBarButtonItem = [BVGAPI barButtonWithIcon:@"locate" target:self action:@selector(centerOnUser)];
|
||||
self.navigationItem.leftBarButtonItem.accessibilityLabel = @"Mein Standort";
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
|
||||
target:self action:@selector(reloadAll)];
|
||||
}
|
||||
@@ -62,7 +61,7 @@ static const CLLocationDegrees kMaxRadarSpan = 0.045; // weiter rausgezoomt ->
|
||||
[super viewDidLayoutSubviews];
|
||||
CGFloat w = self.view.bounds.size.width;
|
||||
CGFloat bottom = self.view.bounds.size.height - self.bottomLayoutGuide.length;
|
||||
self.statusLabel.frame = CGRectMake(w / 2 - 130, bottom - 50, 260, 34);
|
||||
self.statusLabel.frame = CGRectMake(w / 2 - 110, bottom - 42, 220, 28);
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
@@ -273,12 +272,12 @@ static const CLLocationDegrees kMaxRadarSpan = 0.045; // weiter rausgezoomt ->
|
||||
if (a.kind == BVGAnnotationKindVehicle) {
|
||||
v.image = [BVGAPI badgeForLine:a.line];
|
||||
v.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
v.layer.shadowOpacity = 0.35;
|
||||
v.layer.shadowOpacity = 0.25;
|
||||
v.layer.shadowRadius = 2;
|
||||
v.layer.shadowOffset = CGSizeMake(0, 1);
|
||||
v.layer.zPosition = 10;
|
||||
} else {
|
||||
v.image = [MapViewController stopIcon];
|
||||
v.image = [BVGAPI stopSignWithSize:18];
|
||||
v.layer.zPosition = 0;
|
||||
}
|
||||
return v;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#import "CardTableViewController.h"
|
||||
|
||||
@interface SearchViewController : CardTableViewController
|
||||
// Gesetzt = Auswahlmodus (z.B. fuer den Routenplaner): Tippen liefert die Haltestelle zurueck
|
||||
// Gesetzt = Auswahlmodus (z.B. fuer den Routenplaner): Tippen liefert den Ort zurueck.
|
||||
// stop == nil bedeutet "Mein Standort" (nur wenn offerCurrentLocation gesetzt ist).
|
||||
@property (nonatomic, copy) void (^pickHandler)(NSDictionary *stop);
|
||||
// im Auswahlmodus auch Adressen anbieten (z.B. fuer "Zuhause")
|
||||
@property (nonatomic, assign) BOOL allowAddresses;
|
||||
// im Auswahlmodus "Mein Standort" sowie Zuhause/Arbeit als Schnellauswahl zeigen
|
||||
@property (nonatomic, assign) BOOL offerCurrentLocation;
|
||||
@property (nonatomic, assign) BOOL offerSavedPlaces;
|
||||
@property (nonatomic, copy) NSString *pickTitle; // z.B. "Start" / "Ziel"
|
||||
@end
|
||||
@@ -1,12 +1,14 @@
|
||||
#import "SearchViewController.h"
|
||||
#import "StopViewController.h"
|
||||
#import "BVGAPI.h"
|
||||
#import "BVGSettings.h"
|
||||
|
||||
@interface SearchViewController () <UISearchBarDelegate>
|
||||
@property (nonatomic, strong) UISearchBar *searchBar;
|
||||
@property (nonatomic, strong) NSArray *results;
|
||||
@property (nonatomic, copy) NSString *lastQuery;
|
||||
@property (nonatomic, assign) BOOL searching;
|
||||
@property (nonatomic, assign) BOOL focusedOnce;
|
||||
@end
|
||||
|
||||
@implementation SearchViewController
|
||||
@@ -14,18 +16,23 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.results = @[];
|
||||
self.tableView.rowHeight = 58;
|
||||
self.tableView.rowHeight = 52;
|
||||
|
||||
self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 44)];
|
||||
self.searchBar.placeholder = @"Haltestelle, z. B. Alexanderplatz";
|
||||
// Suchfeld: weisses, abgerundetes Feld auf hellgrauem Grund
|
||||
self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 40)];
|
||||
self.searchBar.placeholder = self.allowAddresses ? @"Haltestelle oder Adresse" : @"Haltestelle, z. B. Alexanderplatz";
|
||||
self.searchBar.delegate = self;
|
||||
self.searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.searchBar.searchBarStyle = UISearchBarStyleMinimal;
|
||||
self.searchBar.backgroundImage = [BVGTheme imageWithColor:[BVGTheme background]];
|
||||
[self.searchBar setSearchFieldBackgroundImage:[BVGTheme roundedImageWithColor:[BVGTheme searchField] radius:9]
|
||||
forState:UIControlStateNormal];
|
||||
self.searchBar.searchTextPositionAdjustment = UIOffsetMake(6, 0);
|
||||
self.searchBar.tintColor = [BVGTheme textPrimary];
|
||||
self.searchBar.layoutMargins = UIEdgeInsetsMake(0, [BVGTheme cardInset] - 8, 0, [BVGTheme cardInset] - 8);
|
||||
self.headerAccessory = self.searchBar;
|
||||
if (self.pickHandler) self.navigationItem.title = @"Haltestelle wählen";
|
||||
else self.largeTitle = @"Suche";
|
||||
|
||||
if (self.pickHandler) self.navigationItem.title = self.pickTitle ?: @"Ort wählen";
|
||||
else self.navigationItem.title = @"Haltestelle suchen";
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self.tableView selector:@selector(reloadData)
|
||||
@@ -39,7 +46,14 @@
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self.tableView reloadData]; // "Zuletzt" aktualisieren
|
||||
if (self.pickHandler) [self.searchBar becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
if (!self.focusedOnce) { // nur beim ersten Oeffnen direkt lostippen
|
||||
self.focusedOnce = YES;
|
||||
[self.searchBar becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isSearching {
|
||||
@@ -103,23 +117,23 @@
|
||||
return list.count ? [list componentsJoinedByString:@" · "] : @"Haltestelle";
|
||||
}
|
||||
|
||||
// runde Haltestellen-Marke links in der Liste
|
||||
+ (UIImage *)iconWithText:(NSString *)text fav:(BOOL)fav {
|
||||
// runde Marke links in der Liste (Adresse, Standort, Zuhause ...)
|
||||
+ (UIImage *)iconWithText:(NSString *)text color:(UIColor *)bg {
|
||||
static NSCache *cache;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{ cache = [[NSCache alloc] init]; });
|
||||
NSString *key = [text stringByAppendingString:fav ? @"1" : @"0"];
|
||||
NSString *key = [NSString stringWithFormat:@"%@|%@", text, bg];
|
||||
UIImage *img = [cache objectForKey:key];
|
||||
if (img) return img;
|
||||
|
||||
CGSize size = CGSizeMake(32, 32);
|
||||
CGSize size = CGSizeMake(28, 28);
|
||||
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
||||
[(fav ? [BVGTheme accentSoft] : [BVGTheme background]) setFill];
|
||||
[[UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 32, 32)] fill];
|
||||
UIColor *fg = fav ? [BVGTheme orange] : [UIColor colorWithRed:0 green:0.44 blue:0.29 alpha:1];
|
||||
NSDictionary *attrs = @{ NSFontAttributeName: [BVGTheme font:15 weight:UIFontWeightBold], NSForegroundColorAttributeName: fg };
|
||||
[bg setFill];
|
||||
[[UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 28, 28)] fill];
|
||||
NSDictionary *attrs = @{ NSFontAttributeName: [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold],
|
||||
NSForegroundColorAttributeName: [BVGTheme textPrimary] };
|
||||
CGSize t = [text sizeWithAttributes:attrs];
|
||||
[text drawAtPoint:CGPointMake((32 - t.width) / 2, (32 - t.height) / 2) withAttributes:attrs];
|
||||
[text drawAtPoint:CGPointMake((28 - t.width) / 2, (28 - t.height) / 2) withAttributes:attrs];
|
||||
img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
[cache setObject:img forKey:key];
|
||||
@@ -147,9 +161,23 @@
|
||||
|
||||
#pragma mark - Daten
|
||||
|
||||
// Zeilen: normale Orte, oder Schnellauswahl {kind: current|saved, title, place}
|
||||
- (NSArray *)sections {
|
||||
if ([self isSearching]) return @[ @{ @"title": self.results.count ? @"Treffer" : @"", @"rows": self.results } ];
|
||||
NSMutableArray *s = [NSMutableArray array];
|
||||
|
||||
if (self.pickHandler && (self.offerCurrentLocation || self.offerSavedPlaces)) {
|
||||
NSMutableArray *quick = [NSMutableArray array];
|
||||
if (self.offerCurrentLocation) [quick addObject:@{ @"kind": @"current", @"title": @"Mein Standort" }];
|
||||
if (self.offerSavedPlaces) {
|
||||
for (NSArray *pl in @[@[@"home", @"Zuhause"], @[@"work", @"Arbeit"]]) {
|
||||
NSDictionary *place = [BVGSettings placeForKey:pl[0]];
|
||||
if (place) [quick addObject:@{ @"kind": @"saved", @"key": pl[0], @"title": pl[1], @"place": place }];
|
||||
}
|
||||
}
|
||||
if (quick.count) [s addObject:@{ @"title": @"", @"rows": quick }];
|
||||
}
|
||||
|
||||
NSArray *favs = [BVGAPI favorites];
|
||||
NSArray *recents = [BVGAPI recentStops];
|
||||
if (favs.count) [s addObject:@{ @"title": @"Favoriten", @"rows": favs }];
|
||||
@@ -170,8 +198,8 @@
|
||||
if ([self isSearching]) {
|
||||
[self showEmptyIcon:@"🤷" title:@"Nichts gefunden" text:@"Prüfe die Schreibweise oder versuche einen Teil des Namens."];
|
||||
} else {
|
||||
[self showEmptyIcon:@"🔍" title:@"Haltestelle suchen"
|
||||
text:@"Haltestellen, die du ansiehst, findest du später auch in der iOS-Suche – einfach auf dem Homescreen nach unten wischen."];
|
||||
[self showEmptyIcon:nil title:@"Wohin soll's gehen?"
|
||||
text:@"Tippe mindestens zwei Buchstaben. Mit ☆ gemerkte Haltestellen erscheinen hier und auf der Startseite."];
|
||||
}
|
||||
} else {
|
||||
[self showEmptyIcon:nil title:nil text:nil];
|
||||
@@ -191,31 +219,51 @@
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
CardCell *cell = [tableView dequeueReusableCellWithIdentifier:@"loc"];
|
||||
if (!cell) cell = [[CardCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"loc"];
|
||||
NSDictionary *stop = [self stopAt:indexPath];
|
||||
BOOL fav = [BVGAPI isFavorite:stop[@"id"]];
|
||||
|
||||
BOOL isAddress = ![SearchViewController isStop:stop];
|
||||
cell.imageView.image = [SearchViewController iconWithText:(isAddress ? @"⌂" : (fav ? @"★" : @"H")) fav:fav];
|
||||
cell.textLabel.text = NN(stop[@"name"]) ?: NN(stop[@"address"]);
|
||||
cell.textLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
cell.detailTextLabel.text = isAddress ? @"Adresse" : [SearchViewController productsText:stop];
|
||||
cell.detailTextLabel.font = [BVGTheme font:13 weight:UIFontWeightRegular];
|
||||
NSDictionary *row = [self stopAt:indexPath];
|
||||
cell.textLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
cell.detailTextLabel.font = [BVGTheme font:12 weight:UIFontWeightRegular];
|
||||
cell.accessoryType = self.pickHandler ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator;
|
||||
|
||||
NSString *kind = row[@"kind"];
|
||||
if ([kind isEqualToString:@"current"]) {
|
||||
cell.imageView.image = [SearchViewController iconWithText:@"◉" color:[BVGTheme accentSoft]];
|
||||
cell.textLabel.text = row[@"title"];
|
||||
cell.detailTextLabel.text = @"per GPS";
|
||||
return cell;
|
||||
}
|
||||
if ([kind isEqualToString:@"saved"]) {
|
||||
BOOL home = [row[@"key"] isEqualToString:@"home"];
|
||||
cell.imageView.image = [SearchViewController iconWithText:(home ? @"⌂" : @"✱") color:[BVGTheme accentSoft]];
|
||||
cell.textLabel.text = row[@"title"];
|
||||
cell.detailTextLabel.text = row[@"place"][@"name"];
|
||||
return cell;
|
||||
}
|
||||
|
||||
BOOL isAddress = ![SearchViewController isStop:row];
|
||||
if (isAddress) cell.imageView.image = [SearchViewController iconWithText:@"⌂" color:[BVGTheme background]];
|
||||
else cell.imageView.image = [BVGAPI stopSignWithSize:28];
|
||||
cell.textLabel.text = NN(row[@"name"]) ?: NN(row[@"address"]);
|
||||
NSString *detail = isAddress ? @"Adresse" : [SearchViewController productsText:row];
|
||||
if (!isAddress && [BVGAPI isFavorite:row[@"id"]]) detail = [@"★ " stringByAppendingString:detail];
|
||||
cell.detailTextLabel.text = detail;
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
[self.searchBar resignFirstResponder];
|
||||
NSDictionary *stop = [self stopAt:indexPath];
|
||||
NSDictionary *row = [self stopAt:indexPath];
|
||||
|
||||
if (self.pickHandler) {
|
||||
self.pickHandler([SearchViewController placeFromLocation:stop]);
|
||||
NSString *kind = row[@"kind"];
|
||||
if ([kind isEqualToString:@"current"]) self.pickHandler(nil);
|
||||
else if ([kind isEqualToString:@"saved"]) self.pickHandler(row[@"place"]);
|
||||
else self.pickHandler([SearchViewController placeFromLocation:row]);
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
return;
|
||||
}
|
||||
if (![SearchViewController isStop:stop]) return; // Adressen haben keine Abfahrten
|
||||
StopViewController *vc = [[StopViewController alloc] initWithStopId:stop[@"id"] name:stop[@"name"]];
|
||||
if (![SearchViewController isStop:row]) return; // Adressen haben keine Abfahrten
|
||||
StopViewController *vc = [[StopViewController alloc] initWithStopId:row[@"id"] name:row[@"name"]];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#import "SettingsViewController.h"
|
||||
#import "BVGSettings.h"
|
||||
#import "BVGAPI.h"
|
||||
#import "AlarmsViewController.h"
|
||||
#import "SearchViewController.h"
|
||||
#import <CoreSpotlight/CoreSpotlight.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@@ -14,10 +16,12 @@ static char kSwitchKey; // Zuordnung UISwitch -> Einstellungs-Schluessel
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.largeTitle = @"Einstellungen";
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self action:@selector(close)];
|
||||
self.largeTitle = @"Mehr";
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self.tableView selector:@selector(reloadData)
|
||||
name:BVGAlarmsChangedNotification object:nil];
|
||||
self.sections = @[
|
||||
@{ @"title": @"",
|
||||
@"rows": @[ @{ @"title": @"Wecker", @"open": @"alarms" } ] },
|
||||
@{ @"title": @"Abfahrten",
|
||||
@"rows": @[
|
||||
@{ @"title": @"Suchradius", @"key": @"set.radius",
|
||||
@@ -32,7 +36,7 @@ static char kSwitchKey; // Zuordnung UISwitch -> Einstellungs-Schluessel
|
||||
@{ @"title": @"Einsteigen-Hinweis", @"key": @"set.boardLead",
|
||||
@"values": @[@1, @2, @3, @5], @"labels": @[@"1 min vorher", @"2 min vorher", @"3 min vorher", @"5 min vorher"] } ] },
|
||||
@{ @"title": @"Orte",
|
||||
@"footer": @"Festlegen oder ändern: im Route-Tab das Schnellziel gedrückt halten.",
|
||||
@"footer": @"Werden als Schnellziele in „Verbindung“ angezeigt.",
|
||||
@"rows": @[ @{ @"title": @"🏠 Zuhause", @"place": @"home" },
|
||||
@{ @"title": @"💼 Arbeit", @"place": @"work" } ] },
|
||||
@{ @"title": @"Ansagen",
|
||||
@@ -48,8 +52,13 @@ static char kSwitchKey; // Zuordnung UISwitch -> Einstellungs-Schluessel
|
||||
];
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.tableView];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self.tableView reloadData]; // Wecker-Anzahl, Datenquelle
|
||||
}
|
||||
|
||||
#pragma mark - Table
|
||||
@@ -75,9 +84,15 @@ static char kSwitchKey; // Zuordnung UISwitch -> Einstellungs-Schluessel
|
||||
CardCell *cell = [[CardCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
|
||||
cell.textLabel.text = row[@"title"];
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
cell.detailTextLabel.font = [BVGTheme font:16 weight:UIFontWeightRegular];
|
||||
cell.detailTextLabel.font = [BVGTheme font:15 weight:UIFontWeightRegular];
|
||||
|
||||
if (row[@"values"]) {
|
||||
if (row[@"open"]) {
|
||||
NSUInteger n = [BVGAPI userAlarms].count;
|
||||
cell.textLabel.font = [BVGTheme font:16 weight:UIFontWeightRegular];
|
||||
cell.detailTextLabel.text = n ? [NSString stringWithFormat:@"%lu aktiv", (unsigned long)n] : @"keine";
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
} else if (row[@"values"]) {
|
||||
// aktueller Wert rechts, Antippen oeffnet die Auswahl (wie in den iOS-Einstellungen)
|
||||
NSInteger current = [[NSUserDefaults standardUserDefaults] integerForKey:row[@"key"]];
|
||||
NSUInteger idx = [row[@"values"] indexOfObject:@(current)];
|
||||
@@ -86,8 +101,10 @@ static char kSwitchKey; // Zuordnung UISwitch -> Einstellungs-Schluessel
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
} else if (row[@"place"]) {
|
||||
NSDictionary *place = [BVGSettings placeForKey:row[@"place"]];
|
||||
cell.detailTextLabel.text = place ? place[@"name"] : @"nicht festgelegt";
|
||||
if (place) cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
cell.detailTextLabel.text = place ? place[@"name"] : @"festlegen";
|
||||
cell.detailTextLabel.textColor = place ? [BVGTheme textSecondary] : [BVGTheme link];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
} else if (row[@"switch"]) {
|
||||
UISwitch *sw = [[UISwitch alloc] init];
|
||||
sw.on = [[NSUserDefaults standardUserDefaults] boolForKey:row[@"switch"]];
|
||||
@@ -130,17 +147,39 @@ static char kSwitchKey; // Zuordnung UISwitch -> Einstellungs-Schluessel
|
||||
- (void)switchChanged:(UISwitch *)sw {
|
||||
NSString *key = objc_getAssociatedObject(sw, &kSwitchKey);
|
||||
[BVGSettings setBool:sw.on forKey:key];
|
||||
[BVGAPI syncWidget];
|
||||
if ([key isEqualToString:@"set.speech"] && !sw.on) [BVGAPI stopSpeaking];
|
||||
}
|
||||
|
||||
- (void)pickPlaceForKey:(NSString *)key {
|
||||
__weak SettingsViewController *weakSelf = self;
|
||||
SearchViewController *search = [[SearchViewController alloc] initWithStyle:UITableViewStylePlain];
|
||||
search.allowAddresses = YES;
|
||||
search.pickTitle = [key isEqualToString:@"home"] ? @"Zuhause festlegen" : @"Arbeit festlegen";
|
||||
search.pickHandler = ^(NSDictionary *place) {
|
||||
if (!place) return;
|
||||
[BVGSettings setPlace:place forKey:key];
|
||||
[weakSelf.tableView reloadData];
|
||||
};
|
||||
[self.navigationController pushViewController:search animated:YES];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
NSDictionary *row = self.sections[indexPath.section][@"rows"][indexPath.row];
|
||||
if (row[@"open"]) {
|
||||
[self.navigationController pushViewController:[[AlarmsViewController alloc] initWithStyle:UITableViewStylePlain]
|
||||
animated:YES];
|
||||
return;
|
||||
}
|
||||
if (row[@"values"]) { [self chooseValueForRow:row]; return; }
|
||||
if (row[@"place"]) {
|
||||
if (![BVGSettings placeForKey:row[@"place"]]) return;
|
||||
if (![BVGSettings placeForKey:row[@"place"]]) { [self pickPlaceForKey:row[@"place"]]; return; }
|
||||
UIAlertController *a = [UIAlertController alertControllerWithTitle:row[@"title"] message:nil
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
[a addAction:[UIAlertAction actionWithTitle:@"Ändern …" style:UIAlertActionStyleDefault handler:^(UIAlertAction *x) {
|
||||
[self pickPlaceForKey:row[@"place"]];
|
||||
}]];
|
||||
[a addAction:[UIAlertAction actionWithTitle:@"Ort entfernen" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *x) {
|
||||
[BVGSettings setPlace:nil forKey:row[@"place"]];
|
||||
[self.tableView reloadData];
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
self.filter.onChange = ^(NSInteger idx) { [weakSelf applyFilter]; };
|
||||
|
||||
// darunter: Richtungsfilter (wird pro Haltestelle gemerkt, gilt auch in der Startansicht)
|
||||
UIView *acc = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 82)];
|
||||
UIView *acc = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 76)];
|
||||
[acc addSubview:self.filter];
|
||||
self.directionButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
self.directionButton.frame = CGRectMake([BVGTheme cardInset] + 4, 46, acc.bounds.size.width - 2 * [BVGTheme cardInset] - 8, 32);
|
||||
self.directionButton.frame = CGRectMake([BVGTheme cardInset] + 6, 44, acc.bounds.size.width - 2 * [BVGTheme cardInset] - 12, 28);
|
||||
self.directionButton.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
self.directionButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
self.directionButton.titleLabel.font = [BVGTheme font:15 weight:UIFontWeightMedium];
|
||||
self.directionButton.titleLabel.font = [BVGTheme font:14 weight:UIFontWeightMedium];
|
||||
self.directionButton.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
[self.directionButton addTarget:self action:@selector(chooseDirection) forControlEvents:UIControlEventTouchUpInside];
|
||||
[acc addSubview:self.directionButton];
|
||||
@@ -88,12 +88,11 @@
|
||||
UIBarButtonItem *star = [[UIBarButtonItem alloc] initWithTitle:(fav ? @"★" : @"☆")
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(toggleStar)];
|
||||
[star setTitleTextAttributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:24] } forState:UIControlStateNormal];
|
||||
[star setTitleTextAttributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:20] } forState:UIControlStateNormal];
|
||||
|
||||
star.tintColor = fav ? [BVGTheme orange] : [BVGTheme textSecondary];
|
||||
UIBarButtonItem *speaker = [[UIBarButtonItem alloc] initWithTitle:@"Ansage"
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(announce)];
|
||||
star.tintColor = fav ? [BVGTheme textPrimary] : [BVGTheme headerSecondary];
|
||||
UIBarButtonItem *speaker = [BVGAPI barButtonWithIcon:@"speaker" target:self action:@selector(announce)];
|
||||
speaker.accessibilityLabel = @"Abfahrten ansagen";
|
||||
self.navigationItem.rightBarButtonItems = @[star, speaker];
|
||||
}
|
||||
|
||||
@@ -154,9 +153,9 @@
|
||||
|
||||
- (void)updateDirectionButton {
|
||||
NSString *dir = [BVGSettings directionFilterForStop:self.stopId];
|
||||
[self.directionButton setTitle:(dir ? [NSString stringWithFormat:@"Nur Richtung %@ ✕", dir] : @"Alle Richtungen ▾")
|
||||
[self.directionButton setTitle:(dir ? [NSString stringWithFormat:@"→ nur %@ ✕", dir] : @"Alle Richtungen ▾")
|
||||
forState:UIControlStateNormal];
|
||||
[self.directionButton setTitleColor:(dir ? [BVGTheme orange] : [BVGTheme link]) forState:UIControlStateNormal];
|
||||
[self.directionButton setTitleColor:(dir ? [BVGTheme warningText] : [BVGTheme link]) forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
- (void)chooseDirection {
|
||||
@@ -244,8 +243,8 @@
|
||||
CardCell *cell = [tableView dequeueReusableCellWithIdentifier:@"warn"];
|
||||
if (!cell) cell = [[CardCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"warn"];
|
||||
cell.textLabel.text = [@"⚠︎ " stringByAppendingString:self.warnings[indexPath.row][@"summary"]];
|
||||
cell.textLabel.font = [BVGTheme font:15 weight:UIFontWeightMedium];
|
||||
cell.textLabel.textColor = [UIColor colorWithRed:0.62 green:0.36 blue:0 alpha:1];
|
||||
cell.textLabel.font = [BVGTheme font:14 weight:UIFontWeightMedium];
|
||||
cell.textLabel.textColor = [BVGTheme warningText];
|
||||
cell.textLabel.numberOfLines = 2;
|
||||
cell.cardColor = [BVGTheme accentSoft];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
[self.contentView addSubview:_noteLabel];
|
||||
_timeLabel = [[UILabel alloc] init];
|
||||
_timeLabel.textAlignment = NSTextAlignmentRight;
|
||||
_timeLabel.font = [BVGTheme digits:15 weight:UIFontWeightMedium];
|
||||
_timeLabel.font = [BVGTheme digits:14 weight:UIFontWeightMedium];
|
||||
[self.contentView addSubview:_timeLabel];
|
||||
}
|
||||
return self;
|
||||
@@ -121,7 +121,7 @@
|
||||
[header addSubview:badge];
|
||||
|
||||
UILabel *dir = [[UILabel alloc] initWithFrame:CGRectMake(x, 40, w - 2 * x, 34)];
|
||||
dir.font = [BVGTheme font:26 weight:UIFontWeightBold];
|
||||
dir.font = [BVGTheme font:22 weight:UIFontWeightBold];
|
||||
dir.textColor = [BVGTheme textPrimary];
|
||||
dir.adjustsFontSizeToFitWidth = YES;
|
||||
dir.minimumScaleFactor = 0.6;
|
||||
@@ -226,7 +226,7 @@
|
||||
[cell.timeline setNeedsDisplay];
|
||||
|
||||
cell.nameLabel.text = name;
|
||||
cell.nameLabel.font = (current || mine) ? [BVGTheme font:16 weight:UIFontWeightSemibold] : [BVGTheme font:16 weight:UIFontWeightRegular];
|
||||
cell.nameLabel.font = (current || mine) ? [BVGTheme font:15 weight:UIFontWeightSemibold] : [BVGTheme font:15 weight:UIFontWeightRegular];
|
||||
cell.nameLabel.textColor = past ? [BVGTheme textTertiary] : [BVGTheme textPrimary];
|
||||
|
||||
if (cancelled) { cell.noteLabel.text = @"Halt entfällt"; cell.noteLabel.textColor = [BVGTheme red]; }
|
||||
|
||||
+168
-111
@@ -1,15 +1,64 @@
|
||||
#import "ViewController.h"
|
||||
#import "TripViewController.h"
|
||||
#import "StopViewController.h"
|
||||
#import "SearchViewController.h"
|
||||
#import "DepartureCell.h"
|
||||
#import "BVGAPI.h"
|
||||
#import "BVGSettings.h"
|
||||
#import "SettingsViewController.h"
|
||||
#import "DisruptionsViewController.h"
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
static NSString * const kCacheKey = @"nearbyCache";
|
||||
|
||||
#pragma mark - Kopfzeile einer Haltestellen-Karte
|
||||
|
||||
@interface StopHeaderCell : CardCell
|
||||
@property (nonatomic, strong) UIImageView *signView;
|
||||
@property (nonatomic, strong) UILabel *nameLabel;
|
||||
@property (nonatomic, strong) UILabel *metaLabel;
|
||||
@property (nonatomic, strong) UIButton *starButton;
|
||||
@end
|
||||
|
||||
@implementation StopHeaderCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
_signView = [[UIImageView alloc] initWithImage:[BVGAPI stopSignWithSize:24]];
|
||||
[self.contentView addSubview:_signView];
|
||||
|
||||
_nameLabel = [[UILabel alloc] init];
|
||||
_nameLabel.font = [BVGTheme font:16 weight:UIFontWeightSemibold];
|
||||
_nameLabel.textColor = [BVGTheme textPrimary];
|
||||
_nameLabel.adjustsFontSizeToFitWidth = YES;
|
||||
_nameLabel.minimumScaleFactor = 0.85;
|
||||
[self.contentView addSubview:_nameLabel];
|
||||
|
||||
_metaLabel = [[UILabel alloc] init];
|
||||
_metaLabel.font = [BVGTheme font:12 weight:UIFontWeightRegular];
|
||||
_metaLabel.textColor = [BVGTheme textSecondary];
|
||||
[self.contentView addSubview:_metaLabel];
|
||||
|
||||
_starButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_starButton.titleLabel.font = [UIFont systemFontOfSize:20];
|
||||
[self.contentView addSubview:_starButton];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
CGFloat w = self.contentView.bounds.size.width;
|
||||
self.signView.frame = CGRectMake(12, 12, 24, 24);
|
||||
self.nameLabel.frame = CGRectMake(46, 7, w - 46 - 46, 20);
|
||||
self.metaLabel.frame = CGRectMake(46, 27, w - 46 - 46, 15);
|
||||
self.starButton.frame = CGRectMake(w - 44, 2, 44, 44);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Abfahrten in der Naehe
|
||||
|
||||
@interface ViewController () <CLLocationManagerDelegate>
|
||||
@property (nonatomic, strong) CLLocationManager *locationManager;
|
||||
@property (nonatomic, strong) NSArray *stops; // [NSMutableDictionary: id, name, distance, fav, departures]
|
||||
@@ -31,12 +80,15 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
self.largeTitle = @"Abfahrten";
|
||||
self.largeSubtitle = @"In deiner Nähe";
|
||||
self.stops = @[];
|
||||
self.bannerCount = -1;
|
||||
self.tableView.rowHeight = [DepartureCell height];
|
||||
self.bannerCount = 0;
|
||||
[self buildHeaderAccessory];
|
||||
|
||||
self.refreshControl = [[UIRefreshControl alloc] init];
|
||||
[self.refreshControl addTarget:self action:@selector(refresh) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
UILongPressGestureRecognizer *press = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
|
||||
[self.tableView addGestureRecognizer:press];
|
||||
|
||||
self.locationManager = [[CLLocationManager alloc] init];
|
||||
self.locationManager.delegate = self;
|
||||
self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
|
||||
@@ -47,14 +99,8 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
[nc addObserver:self.tableView selector:@selector(reloadData) name:BVGAlarmsChangedNotification object:nil];
|
||||
[nc addObserver:self selector:@selector(settingsChanged) name:BVGSettingsChangedNotification object:nil];
|
||||
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Ansage"
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(announceNearest)];
|
||||
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"⚙︎"
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self action:@selector(openSettings)];
|
||||
[self.navigationItem.leftBarButtonItem setTitleTextAttributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:22] }
|
||||
forState:UIControlStateNormal];
|
||||
self.navigationItem.rightBarButtonItem = [BVGAPI barButtonWithIcon:@"speaker" target:self action:@selector(announceNearest)];
|
||||
self.navigationItem.rightBarButtonItem.accessibilityLabel = @"Abfahrten ansagen";
|
||||
|
||||
// Sofort die letzten bekannten Daten zeigen, waehrend neu geladen wird
|
||||
if (![self loadCache]) [self showEmptyIcon:@"📍" title:@"Einen Moment" text:@"Suche Haltestellen in deiner Nähe …"];
|
||||
@@ -92,17 +138,60 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)openSettings {
|
||||
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:
|
||||
[[SettingsViewController alloc] initWithStyle:UITableViewStyleGrouped]];
|
||||
[self presentViewController:nav animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)settingsChanged {
|
||||
[self.tableView reloadData];
|
||||
[self refresh];
|
||||
}
|
||||
|
||||
#pragma mark - Kopfbereich: Suchfeld + Stoerungs-Hinweis
|
||||
|
||||
- (void)buildHeaderAccessory {
|
||||
CGFloat w = self.view.bounds.size.width;
|
||||
CGFloat inset = [BVGTheme cardInset];
|
||||
CGFloat h = 36 + (self.bannerCount > 0 ? 42 : 0);
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, h)];
|
||||
|
||||
// weisses "Suchfeld" - oeffnet die Haltestellensuche
|
||||
UIButton *search = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
search.frame = CGRectMake(inset, 0, w - 2 * inset, 36);
|
||||
search.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[search setBackgroundImage:[BVGTheme roundedImageWithColor:[BVGTheme searchField] radius:9] forState:UIControlStateNormal];
|
||||
[search setBackgroundImage:[BVGTheme roundedImageWithColor:[BVGTheme cardPressed] radius:9] forState:UIControlStateHighlighted];
|
||||
search.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
search.contentEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 10);
|
||||
search.titleEdgeInsets = UIEdgeInsetsMake(0, 6, 0, -6);
|
||||
[search setImage:[BVGAPI smallIconNamed:@"search" size:15] forState:UIControlStateNormal];
|
||||
search.tintColor = [BVGTheme textSecondary];
|
||||
search.titleLabel.font = [BVGTheme font:15 weight:UIFontWeightRegular];
|
||||
[search setTitle:@"Haltestelle suchen" forState:UIControlStateNormal];
|
||||
[search setTitleColor:[BVGTheme textSecondary] forState:UIControlStateNormal];
|
||||
search.accessibilityLabel = @"Haltestelle suchen";
|
||||
[search addTarget:self action:@selector(openSearch) forControlEvents:UIControlEventTouchUpInside];
|
||||
[v addSubview:search];
|
||||
|
||||
// schwarzer Hinweis-Balken bei Stoerungen an den angezeigten Haltestellen
|
||||
if (self.bannerCount > 0) {
|
||||
UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
b.frame = CGRectMake(inset, 42, w - 2 * inset, 34);
|
||||
b.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[b setBackgroundImage:[BVGTheme roundedImageWithColor:[BVGTheme accentSoft] radius:9] forState:UIControlStateNormal];
|
||||
b.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
b.contentEdgeInsets = UIEdgeInsetsMake(0, 12, 0, 12);
|
||||
b.titleLabel.font = [BVGTheme font:13 weight:UIFontWeightSemibold];
|
||||
[b setTitleColor:[BVGTheme warningText] forState:UIControlStateNormal];
|
||||
[b setTitle:[NSString stringWithFormat:@"⚠︎ %ld %@ an deinen Haltestellen ›", (long)self.bannerCount,
|
||||
self.bannerCount == 1 ? @"Hinweis" : @"Hinweise"] forState:UIControlStateNormal];
|
||||
[b addTarget:self action:@selector(openDisruptions) forControlEvents:UIControlEventTouchUpInside];
|
||||
[v addSubview:b];
|
||||
}
|
||||
self.headerAccessory = v;
|
||||
}
|
||||
|
||||
- (void)openSearch {
|
||||
SearchViewController *search = [[SearchViewController alloc] initWithStyle:UITableViewStylePlain];
|
||||
[self.navigationController pushViewController:search animated:YES];
|
||||
}
|
||||
|
||||
#pragma mark - Cache & Offline-Hinweis
|
||||
|
||||
- (void)saveCache {
|
||||
@@ -131,42 +220,24 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
NSTimeInterval age = self.dataTime ? -[self.dataTime timeIntervalSinceNow] : 0;
|
||||
if (self.offline && self.dataTime) {
|
||||
self.largeSubtitleColor = [BVGTheme red];
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"Offline · Daten von %@", [BVGAPI hhmm:self.dataTime]];
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"Offline · Daten von %@ Uhr", [BVGAPI hhmm:self.dataTime]];
|
||||
} else if (self.dataTime && age > 120) {
|
||||
self.largeSubtitleColor = [BVGTheme orange];
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"Stand %@ · wird aktualisiert …", [BVGAPI hhmm:self.dataTime]];
|
||||
self.largeSubtitleColor = [BVGTheme warningText];
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"Stand %@ Uhr · wird aktualisiert …", [BVGAPI hhmm:self.dataTime]];
|
||||
} else if (self.dataTime) {
|
||||
self.largeSubtitleColor = nil;
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"In deiner Nähe · Live %@", [BVGAPI hhmm:self.dataTime]];
|
||||
self.largeSubtitle = [NSString stringWithFormat:@"In deiner Nähe · live, %@ Uhr", [BVGAPI hhmm:self.dataTime]];
|
||||
} else {
|
||||
self.largeSubtitleColor = nil;
|
||||
self.largeSubtitle = @"In deiner Nähe";
|
||||
}
|
||||
}
|
||||
|
||||
// gelbes Banner unter dem Titel, wenn es an den Haltestellen Stoerungen gibt
|
||||
- (void)updateDisruptionBanner {
|
||||
NSInteger count = [DisruptionsViewController countInCollection:[DisruptionsViewController collectFromStops:self.stops]];
|
||||
if (count == self.bannerCount) return;
|
||||
self.bannerCount = count;
|
||||
if (count == 0) { self.headerAccessory = nil; return; }
|
||||
|
||||
CGFloat w = self.view.bounds.size.width;
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 52)];
|
||||
UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
b.frame = CGRectMake([BVGTheme cardInset], 4, w - 2 * [BVGTheme cardInset], 44);
|
||||
b.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
b.backgroundColor = [BVGTheme accentSoft];
|
||||
b.layer.cornerRadius = [BVGTheme cardRadius];
|
||||
b.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
b.contentEdgeInsets = UIEdgeInsetsMake(0, 14, 0, 14);
|
||||
b.titleLabel.font = [BVGTheme font:15 weight:UIFontWeightSemibold];
|
||||
[b setTitleColor:[UIColor colorWithRed:0.62 green:0.36 blue:0 alpha:1] forState:UIControlStateNormal];
|
||||
[b setTitle:[NSString stringWithFormat:@"⚠︎ %ld %@ an deinen Haltestellen ›", (long)count,
|
||||
count == 1 ? @"Hinweis" : @"Hinweise"] forState:UIControlStateNormal];
|
||||
[b addTarget:self action:@selector(openDisruptions) forControlEvents:UIControlEventTouchUpInside];
|
||||
[v addSubview:b];
|
||||
self.headerAccessory = v;
|
||||
[self buildHeaderAccessory];
|
||||
}
|
||||
|
||||
- (void)openDisruptions {
|
||||
@@ -197,10 +268,11 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
[self announceStop:best];
|
||||
}
|
||||
|
||||
- (void)announceFromHeader:(UILongPressGestureRecognizer *)press {
|
||||
// Haltestellen-Kopf gedrueckt halten -> Ansage fuer diese Haltestelle
|
||||
- (void)longPress:(UILongPressGestureRecognizer *)press {
|
||||
if (press.state != UIGestureRecognizerStateBegan) return;
|
||||
NSInteger section = press.view.tag;
|
||||
if (section < (NSInteger)self.stops.count) [self announceStop:self.stops[section]];
|
||||
NSIndexPath *ip = [self.tableView indexPathForRowAtPoint:[press locationInView:self.tableView]];
|
||||
if (ip && ip.row == 0 && ip.section < (NSInteger)self.stops.count) [self announceStop:self.stops[ip.section]];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder {
|
||||
@@ -236,12 +308,6 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
[BVGAPI setFavorite:![stop[@"fav"] boolValue] stopId:stop[@"id"] name:stop[@"name"]];
|
||||
}
|
||||
|
||||
- (void)openStopFromHeader:(UITapGestureRecognizer *)tap {
|
||||
NSInteger section = tap.view.tag;
|
||||
if (section >= (NSInteger)self.stops.count) return;
|
||||
[self openStop:self.stops[section]];
|
||||
}
|
||||
|
||||
- (void)openStop:(NSDictionary *)stop {
|
||||
StopViewController *vc = [[StopViewController alloc] initWithStopId:stop[@"id"] name:stop[@"name"]];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
@@ -330,10 +396,10 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
[self showEmptyIcon:@"📡" title:@"Keine Verbindung" text:@"Zum Wiederholen nach unten ziehen oder das iPhone schütteln."];
|
||||
} else if (hadLocation) {
|
||||
[self showEmptyIcon:@"🔍" title:@"Nichts in der Nähe"
|
||||
text:[NSString stringWithFormat:@"Keine Haltestellen im Umkreis von %ld m. Den Radius kannst du in den Einstellungen vergrößern.", (long)[BVGSettings radius]]];
|
||||
text:[NSString stringWithFormat:@"Keine Haltestellen im Umkreis von %ld m. Den Radius kannst du unter „Mehr“ vergrößern.", (long)[BVGSettings radius]]];
|
||||
} else {
|
||||
[self showEmptyIcon:@"📍" title:@"Kein Standort"
|
||||
text:@"Erlaube den Standortzugriff in den iOS-Einstellungen oder lege Favoriten über die Suche an."];
|
||||
text:@"Erlaube den Standortzugriff in den iOS-Einstellungen oder suche oben eine Haltestelle und merke sie dir mit ☆."];
|
||||
}
|
||||
[self finish];
|
||||
return;
|
||||
@@ -382,8 +448,6 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.loading = NO;
|
||||
[self.refreshControl endRefreshing];
|
||||
self.refreshControl.attributedTitle = [[NSAttributedString alloc]
|
||||
initWithString:[NSString stringWithFormat:@"Stand %@", [BVGAPI hhmm:self.dataTime ?: [NSDate date]]]];
|
||||
[self updateStatusHeader];
|
||||
});
|
||||
}
|
||||
@@ -404,15 +468,27 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
return deps.count > max ? [deps subarrayWithRange:NSMakeRange(0, max)] : deps;
|
||||
}
|
||||
|
||||
// Zeilen pro Karte: 0 = Haltestelle · 1..n = Abfahrten (oder Leer-Hinweis) · letzte = "Alle Abfahrten"
|
||||
- (NSDictionary *)departureAt:(NSIndexPath *)ip {
|
||||
NSArray *deps = [self departuresInSection:ip.section];
|
||||
return ip.row < (NSInteger)deps.count ? deps[ip.row] : nil;
|
||||
NSInteger i = ip.row - 1;
|
||||
return (i >= 0 && i < (NSInteger)deps.count) ? deps[i] : nil;
|
||||
}
|
||||
|
||||
- (BOOL)isHeaderRow:(NSIndexPath *)ip {
|
||||
return ip.row == 0;
|
||||
}
|
||||
|
||||
- (BOOL)isMoreRow:(NSIndexPath *)ip {
|
||||
return ip.row == [self tableView:self.tableView numberOfRowsInSection:ip.section] - 1;
|
||||
}
|
||||
|
||||
// Luftlinie * 1,3 (Umwege) bei ca. 80 m/min Gehtempo, mindestens 1 min
|
||||
- (NSInteger)walkMinutesToStop:(NSDictionary *)stop {
|
||||
double dist = [stop[@"distance"] doubleValue];
|
||||
return MAX(1, (NSInteger)ceil(dist * 1.3 / 80.0));
|
||||
}
|
||||
|
||||
#pragma mark - Table
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
@@ -420,69 +496,56 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
|
||||
return 62;
|
||||
return section == 0 ? 4 : 12;
|
||||
}
|
||||
|
||||
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
|
||||
NSDictionary *s = self.stops[section];
|
||||
CGFloat w = tableView.bounds.size.width;
|
||||
CGFloat x = [BVGTheme cardInset] + 4;
|
||||
|
||||
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, w, 62)];
|
||||
v.tag = section;
|
||||
[v addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openStopFromHeader:)]];
|
||||
[v addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(announceFromHeader:)]];
|
||||
v.isAccessibilityElement = YES;
|
||||
v.accessibilityTraits = UIAccessibilityTraitHeader | UIAccessibilityTraitButton;
|
||||
v.accessibilityLabel = s[@"name"];
|
||||
|
||||
UILabel *name = [[UILabel alloc] initWithFrame:CGRectMake(x, 16, w - x - 56, 25)];
|
||||
name.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
name.font = [BVGTheme font:21 weight:UIFontWeightBold];
|
||||
name.textColor = [BVGTheme textPrimary];
|
||||
name.text = s[@"name"];
|
||||
[v addSubview:name];
|
||||
|
||||
UILabel *meta = [[UILabel alloc] initWithFrame:CGRectMake(x, 41, w - x - 56, 16)];
|
||||
meta.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
meta.font = [BVGTheme font:13 weight:UIFontWeightMedium];
|
||||
meta.textColor = [BVGTheme textSecondary];
|
||||
NSNumber *dist = s[@"distance"];
|
||||
NSMutableArray *parts = [NSMutableArray array];
|
||||
if ([s[@"fav"] boolValue]) [parts addObject:@"★ Favorit"];
|
||||
NSString *dirFilter = [BVGSettings directionFilterForStop:s[@"id"]];
|
||||
if (dirFilter) [parts addObject:[@"→ " stringByAppendingString:dirFilter]];
|
||||
if (dist) [parts addObject:[NSString stringWithFormat:@"%@ m · %ld min zu Fuß", dist, (long)[self walkMinutesToStop:s]]];
|
||||
meta.text = [parts componentsJoinedByString:@" · "];
|
||||
[v addSubview:meta];
|
||||
|
||||
UIButton *star = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
star.frame = CGRectMake(w - [BVGTheme cardInset] - 44, 14, 44, 44);
|
||||
star.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
|
||||
star.titleLabel.font = [UIFont systemFontOfSize:24];
|
||||
[star setTitle:([s[@"fav"] boolValue] ? @"★" : @"☆") forState:UIControlStateNormal];
|
||||
[star setTitleColor:([s[@"fav"] boolValue] ? [BVGTheme orange] : [BVGTheme textTertiary]) forState:UIControlStateNormal];
|
||||
star.accessibilityLabel = [s[@"fav"] boolValue] ? @"Favorit entfernen" : @"Als Favorit merken";
|
||||
star.tag = section;
|
||||
[star addTarget:self action:@selector(toggleFavorite:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[v addSubview:star];
|
||||
return v;
|
||||
return [[UIView alloc] initWithFrame:CGRectZero];
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return MAX([self departuresInSection:section].count, 1) + 1; // + "Alle Abfahrten"
|
||||
return 1 + MAX([self departuresInSection:section].count, 1) + 1;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [self isMoreRow:indexPath] ? 44 : [DepartureCell height];
|
||||
if ([self isHeaderRow:indexPath]) return 48;
|
||||
if ([self isMoreRow:indexPath]) return 40;
|
||||
return [DepartureCell height];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSDictionary *stop = self.stops[indexPath.section];
|
||||
|
||||
if ([self isHeaderRow:indexPath]) {
|
||||
StopHeaderCell *cell = [tableView dequeueReusableCellWithIdentifier:@"stop"];
|
||||
if (!cell) {
|
||||
cell = [[StopHeaderCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"stop"];
|
||||
[cell.starButton addTarget:self action:@selector(toggleFavorite:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
BOOL fav = [stop[@"fav"] boolValue];
|
||||
cell.nameLabel.text = [BVGAPI shortName:stop[@"name"]];
|
||||
NSMutableArray *parts = [NSMutableArray array];
|
||||
if (stop[@"distance"]) [parts addObject:[NSString stringWithFormat:@"%@ m · %ld min zu Fuß",
|
||||
stop[@"distance"], (long)[self walkMinutesToStop:stop]]];
|
||||
else if (fav) [parts addObject:@"Favorit"];
|
||||
NSString *dirFilter = [BVGSettings directionFilterForStop:stop[@"id"]];
|
||||
if (dirFilter) [parts addObject:[@"nur → " stringByAppendingString:dirFilter]];
|
||||
cell.metaLabel.text = [parts componentsJoinedByString:@" · "];
|
||||
cell.starButton.tag = indexPath.section;
|
||||
[cell.starButton setTitle:(fav ? @"★" : @"☆") forState:UIControlStateNormal];
|
||||
[cell.starButton setTitleColor:(fav ? [BVGTheme orange] : [BVGTheme textTertiary]) forState:UIControlStateNormal];
|
||||
cell.starButton.accessibilityLabel = fav ? @"Favorit entfernen" : @"Als Favorit merken";
|
||||
cell.accessibilityLabel = stop[@"name"];
|
||||
cell.accessibilityHint = @"Doppeltippen für alle Abfahrten";
|
||||
cell.accessibilityTraits = UIAccessibilityTraitHeader | UIAccessibilityTraitButton;
|
||||
return cell;
|
||||
}
|
||||
|
||||
if ([self isMoreRow:indexPath]) {
|
||||
CardCell *cell = [tableView dequeueReusableCellWithIdentifier:@"more"];
|
||||
if (!cell) cell = [[CardCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"more"];
|
||||
cell.textLabel.text = @"Alle Abfahrten & Hinweise";
|
||||
cell.textLabel.font = [BVGTheme font:15 weight:UIFontWeightMedium];
|
||||
cell.textLabel.text = @"Alle Abfahrten";
|
||||
cell.textLabel.font = [BVGTheme font:14 weight:UIFontWeightRegular];
|
||||
cell.textLabel.textColor = [BVGTheme link];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
return cell;
|
||||
@@ -494,7 +557,6 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
if (d) {
|
||||
[cell configureWithDeparture:d];
|
||||
// zu Fuss nicht mehr erreichbar? -> ausgrauen
|
||||
NSDictionary *stop = self.stops[indexPath.section];
|
||||
NSDate *when = [BVGAPI whenOfDeparture:d];
|
||||
BOOL tooLate = stop[@"distance"] && when &&
|
||||
[when timeIntervalSinceNow] / 60.0 < [self walkMinutesToStop:stop];
|
||||
@@ -505,16 +567,11 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
return cell;
|
||||
}
|
||||
|
||||
// Luftlinie * 1,3 (Umwege) bei ca. 80 m/min Gehtempo, mindestens 1 min
|
||||
- (NSInteger)walkMinutesToStop:(NSDictionary *)stop {
|
||||
double dist = [stop[@"distance"] doubleValue];
|
||||
return MAX(1, (NSInteger)ceil(dist * 1.3 / 80.0));
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
if (indexPath.section >= (NSInteger)self.stops.count) return;
|
||||
|
||||
if ([self isMoreRow:indexPath]) {
|
||||
if ([self isHeaderRow:indexPath] || [self isMoreRow:indexPath]) {
|
||||
[self openStop:self.stops[indexPath.section]];
|
||||
return;
|
||||
}
|
||||
@@ -532,7 +589,7 @@ static NSString * const kCacheKey = @"nearbyCache";
|
||||
#pragma mark - Wischen → Wecker
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if ([self isMoreRow:indexPath]) return NO;
|
||||
if ([self isHeaderRow:indexPath] || [self isMoreRow:indexPath]) return NO;
|
||||
NSDictionary *d = [self departureAt:indexPath];
|
||||
return d && NN(d[@"tripId"]) && ![NN(d[@"cancelled"]) boolValue];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>de</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>BVG Abfahrten</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>BVGWidget</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.serverlele.BVGNearby.widget</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>BVGWidget</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>9.0</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
</array>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Ohne Favoriten zeigt das Widget die Abfahrten an den nächsten Haltestellen.</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widget-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>TodayViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,333 @@
|
||||
// Widget fuer die iOS-9-Mitteilungszentrale ("Heute"):
|
||||
// zeigt die naechsten Abfahrten der Favoriten - ohne Favoriten die naechsten Haltestellen per Standort.
|
||||
// Wird von deploy.sh direkt mit clang gebaut (kein eigenes Xcode-Target noetig).
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <NotificationCenter/NotificationCenter.h>
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
static NSString * const kAppGroup = @"group.com.serverlele.BVGNearby";
|
||||
static NSString * const kOwnBase = @"https://api.serverlele.de/live_navi/v6";
|
||||
static NSString * const kPublicBase = @"https://v6.bvg.transport.rest";
|
||||
static NSString * const kCacheKey = @"widgetCache";
|
||||
static const NSInteger kMaxStops = 2, kPerStop = 3;
|
||||
static const CGFloat kRowH = 30, kHeadH = 26;
|
||||
|
||||
static inline id NN(id v) { return (v == [NSNull null]) ? nil : v; }
|
||||
|
||||
@interface TodayViewController : UIViewController <NCWidgetProviding, CLLocationManagerDelegate>
|
||||
@property (nonatomic, strong) NSArray *stops; // [{id, name, departures:[...]}]
|
||||
@property (nonatomic, strong) UILabel *messageLabel;
|
||||
@property (nonatomic, strong) CLLocationManager *locationManager;
|
||||
@property (nonatomic, copy) void (^pending)(NCUpdateResult);
|
||||
@property (nonatomic, assign) CGFloat laidOutWidth;
|
||||
@end
|
||||
|
||||
@implementation TodayViewController
|
||||
|
||||
#pragma mark - Hilfen
|
||||
|
||||
+ (UIColor *)colorFromHex:(NSString *)hex {
|
||||
if (![hex isKindOfClass:[NSString class]]) return nil;
|
||||
NSString *h = [hex stringByReplacingOccurrencesOfString:@"#" withString:@""];
|
||||
if (h.length != 6) return nil;
|
||||
unsigned int rgb = 0;
|
||||
[[NSScanner scannerWithString:h] scanHexInt:&rgb];
|
||||
return [UIColor colorWithRed:((rgb >> 16) & 0xFF) / 255.0 green:((rgb >> 8) & 0xFF) / 255.0 blue:(rgb & 0xFF) / 255.0 alpha:1];
|
||||
}
|
||||
|
||||
+ (UIColor *)colorForProduct:(NSString *)p {
|
||||
if ([p isEqualToString:@"suburban"]) return [UIColor colorWithRed:0.00 green:0.55 blue:0.24 alpha:1];
|
||||
if ([p isEqualToString:@"subway"]) return [UIColor colorWithRed:0.07 green:0.36 blue:0.66 alpha:1];
|
||||
if ([p isEqualToString:@"tram"]) return [UIColor colorWithRed:0.80 green:0.04 blue:0.13 alpha:1];
|
||||
if ([p isEqualToString:@"bus"]) return [UIColor colorWithRed:0.58 green:0.15 blue:0.43 alpha:1];
|
||||
if ([p isEqualToString:@"ferry"]) return [UIColor colorWithRed:0.00 green:0.50 blue:0.73 alpha:1];
|
||||
return [UIColor colorWithRed:0.89 green:0.00 blue:0.10 alpha:1];
|
||||
}
|
||||
|
||||
+ (UIImage *)badgeForLine:(NSDictionary *)line {
|
||||
NSString *name = NN(line[@"name"]) ?: @"?";
|
||||
NSString *product = NN(line[@"product"]) ?: @"";
|
||||
NSDictionary *c = NN(line[@"color"]);
|
||||
UIColor *bg = [c isKindOfClass:[NSDictionary class]] ? [self colorFromHex:NN(c[@"bg"])] : nil;
|
||||
UIColor *fg = [c isKindOfClass:[NSDictionary class]] ? [self colorFromHex:NN(c[@"fg"])] : nil;
|
||||
NSDictionary *attrs = @{ NSFontAttributeName: [UIFont systemFontOfSize:12 weight:UIFontWeightBold],
|
||||
NSForegroundColorAttributeName: fg ?: [UIColor whiteColor] };
|
||||
CGSize t = [name sizeWithAttributes:attrs];
|
||||
CGSize size = CGSizeMake(MAX(34, ceil(t.width) + 10), 19);
|
||||
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
||||
CGFloat r = [product isEqualToString:@"suburban"] ? size.height / 2 : ([product isEqualToString:@"subway"] ? 3 : 4);
|
||||
[(bg ?: [self colorForProduct:product]) setFill];
|
||||
[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, size.width, size.height) cornerRadius:r] fill];
|
||||
[name drawAtPoint:CGPointMake((size.width - t.width) / 2, (size.height - t.height) / 2) withAttributes:attrs];
|
||||
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return img;
|
||||
}
|
||||
|
||||
+ (NSString *)shortName:(NSString *)name {
|
||||
if (![name isKindOfClass:[NSString class]]) return @"";
|
||||
return [name stringByReplacingOccurrencesOfString:@"\\s*\\((Berlin|Bln)\\)" withString:@""
|
||||
options:NSRegularExpressionSearch range:NSMakeRange(0, name.length)];
|
||||
}
|
||||
|
||||
+ (NSDate *)dateFromISO:(id)value {
|
||||
if (![value isKindOfClass:[NSString class]]) return nil;
|
||||
static NSDateFormatter *f;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
f = [[NSDateFormatter alloc] init];
|
||||
f.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
||||
f.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ";
|
||||
});
|
||||
return [f dateFromString:value];
|
||||
}
|
||||
|
||||
+ (NSString *)encode:(NSString *)s {
|
||||
NSCharacterSet *allowed = [NSCharacterSet characterSetWithCharactersInString:
|
||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"];
|
||||
return [s stringByAddingPercentEncodingWithAllowedCharacters:allowed] ?: @"";
|
||||
}
|
||||
|
||||
- (NSUserDefaults *)shared {
|
||||
return [[NSUserDefaults alloc] initWithSuiteName:kAppGroup];
|
||||
}
|
||||
|
||||
#pragma mark - Netzwerk (eigener Server, sonst transport.rest)
|
||||
|
||||
- (void)getJSON:(NSString *)path completion:(void (^)(id json))completion {
|
||||
BOOL own = ![[self shared] objectForKey:@"ownServer"] || [[self shared] boolForKey:@"ownServer"];
|
||||
NSArray *bases = own ? @[kOwnBase, kPublicBase] : @[kPublicBase];
|
||||
[self getJSON:path bases:bases index:0 completion:completion];
|
||||
}
|
||||
|
||||
- (void)getJSON:(NSString *)path bases:(NSArray *)bases index:(NSUInteger)i completion:(void (^)(id json))completion {
|
||||
if (i >= bases.count) { completion(nil); return; }
|
||||
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[bases[i] stringByAppendingString:path]]];
|
||||
req.timeoutInterval = 10;
|
||||
[req setValue:@"BVGNearby-Widget/1.0 (iOS 9; private app)" forHTTPHeaderField:@"User-Agent"];
|
||||
[[[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
|
||||
NSInteger code = [resp isKindOfClass:[NSHTTPURLResponse class]] ? ((NSHTTPURLResponse *)resp).statusCode : 0;
|
||||
id json = (data && code == 200) ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil;
|
||||
if (!json) { [self getJSON:path bases:bases index:i + 1 completion:completion]; return; }
|
||||
completion(json);
|
||||
}] resume];
|
||||
}
|
||||
|
||||
#pragma mark - Lebenszyklus
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.messageLabel = [[UILabel alloc] init];
|
||||
self.messageLabel.font = [UIFont systemFontOfSize:13];
|
||||
self.messageLabel.textColor = [UIColor colorWithWhite:1 alpha:0.7];
|
||||
self.messageLabel.numberOfLines = 0;
|
||||
[self.view addSubview:self.messageLabel];
|
||||
|
||||
// letzte Daten sofort zeigen
|
||||
NSData *cache = [[NSUserDefaults standardUserDefaults] objectForKey:kCacheKey];
|
||||
NSArray *stops = [cache isKindOfClass:[NSData class]] ? [NSJSONSerialization JSONObjectWithData:cache options:0 error:nil] : nil;
|
||||
if ([stops isKindOfClass:[NSArray class]] && stops.count) [self showStops:stops];
|
||||
else [self showMessage:@"Lade Abfahrten …"];
|
||||
|
||||
[self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]];
|
||||
}
|
||||
|
||||
// Breite steht erst nach dem Layout fest -> dann einmal neu aufbauen
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
CGFloat w = self.view.bounds.size.width;
|
||||
if (w > 0 && fabs(w - self.laidOutWidth) > 0.5) {
|
||||
self.laidOutWidth = w;
|
||||
if (self.stops.count) [self showStops:self.stops];
|
||||
}
|
||||
}
|
||||
|
||||
// iOS 9 rueckt Widgets links weit ein - etwas weniger, damit mehr Platz fuer die Richtung bleibt
|
||||
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
|
||||
return UIEdgeInsetsMake(0, 16, 0, 0);
|
||||
}
|
||||
|
||||
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
|
||||
NSArray *favs = [[self shared] arrayForKey:@"favorites"];
|
||||
if (favs.count) {
|
||||
NSMutableArray *list = [NSMutableArray array];
|
||||
for (NSDictionary *f in favs) {
|
||||
if (list.count >= kMaxStops) break;
|
||||
if (f[@"id"]) [list addObject:@{ @"id": f[@"id"], @"name": f[@"name"] ?: @"?" }];
|
||||
}
|
||||
[self loadDeparturesFor:list completion:completionHandler];
|
||||
return;
|
||||
}
|
||||
// keine Favoriten (oder App Group nicht verfuegbar): naechste Haltestellen per Standort
|
||||
self.pending = completionHandler;
|
||||
if (!self.locationManager) {
|
||||
self.locationManager = [[CLLocationManager alloc] init];
|
||||
self.locationManager.delegate = self;
|
||||
self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
|
||||
}
|
||||
CLAuthorizationStatus st = [CLLocationManager authorizationStatus];
|
||||
if (st == kCLAuthorizationStatusDenied || st == kCLAuthorizationStatusRestricted) {
|
||||
[self showMessage:@"Merke dir in der App Haltestellen mit ☆ – sie erscheinen dann hier."];
|
||||
[self finish:NCUpdateResultNoData];
|
||||
return;
|
||||
}
|
||||
if (st == kCLAuthorizationStatusNotDetermined) [self.locationManager requestWhenInUseAuthorization];
|
||||
[self.locationManager requestLocation];
|
||||
}
|
||||
|
||||
- (void)finish:(NCUpdateResult)result {
|
||||
void (^h)(NCUpdateResult) = self.pending;
|
||||
self.pending = nil;
|
||||
if (h) h(result);
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
|
||||
if (!self.pending) return; // nur einmal pro Aktualisierung
|
||||
CLLocationCoordinate2D c = locations.lastObject.coordinate;
|
||||
NSString *path = [NSString stringWithFormat:@"/locations/nearby?latitude=%.6f&longitude=%.6f&results=%ld&distance=1000&poi=false&addresses=false",
|
||||
c.latitude, c.longitude, (long)kMaxStops];
|
||||
[self getJSON:path completion:^(id json) {
|
||||
NSMutableArray *list = [NSMutableArray array];
|
||||
if ([json isKindOfClass:[NSArray class]]) {
|
||||
for (NSDictionary *s in json) {
|
||||
if ([s isKindOfClass:[NSDictionary class]] && NN(s[@"id"])) [list addObject:@{ @"id": s[@"id"], @"name": NN(s[@"name"]) ?: @"?" }];
|
||||
}
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
void (^h)(NCUpdateResult) = self.pending;
|
||||
self.pending = nil;
|
||||
if (!list.count) {
|
||||
[self showMessage:@"Keine Haltestellen in der Nähe gefunden."];
|
||||
if (h) h(NCUpdateResultFailed);
|
||||
return;
|
||||
}
|
||||
[self loadDeparturesFor:list completion:h];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
|
||||
if (!self.pending) return;
|
||||
[self showMessage:@"Standort nicht verfügbar. Merke dir in der App Haltestellen mit ☆."];
|
||||
[self finish:NCUpdateResultFailed];
|
||||
}
|
||||
|
||||
#pragma mark - Abfahrten
|
||||
|
||||
- (void)loadDeparturesFor:(NSArray *)stops completion:(void (^)(NCUpdateResult))completion {
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
for (NSDictionary *s in stops) [result addObject:[s mutableCopy]];
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
__block BOOL anyOK = NO;
|
||||
for (NSMutableDictionary *s in result) {
|
||||
dispatch_group_enter(group);
|
||||
NSString *path = [NSString stringWithFormat:@"/stops/%@/departures?duration=60&results=%ld", s[@"id"], (long)(kPerStop + 3)];
|
||||
[self getJSON:path completion:^(id json) {
|
||||
NSArray *deps = ([json isKindOfClass:[NSDictionary class]] && [json[@"departures"] isKindOfClass:[NSArray class]])
|
||||
? json[@"departures"] : nil;
|
||||
@synchronized (s) {
|
||||
if (deps) { s[@"departures"] = deps; anyOK = YES; }
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
if (anyOK) {
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:result options:0 error:nil];
|
||||
if (data) [[NSUserDefaults standardUserDefaults] setObject:data forKey:kCacheKey];
|
||||
[self showStops:result];
|
||||
} else if (!self.stops.count) {
|
||||
[self showMessage:@"Keine Verbindung zum Server."];
|
||||
}
|
||||
if (completion) completion(anyOK ? NCUpdateResultNewData : NCUpdateResultFailed);
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Darstellung
|
||||
|
||||
- (void)showMessage:(NSString *)text {
|
||||
for (UIView *v in self.view.subviews) if (v != self.messageLabel) [v removeFromSuperview];
|
||||
self.stops = @[];
|
||||
self.messageLabel.hidden = NO;
|
||||
self.messageLabel.text = text;
|
||||
CGFloat w = self.view.bounds.size.width ?: [UIScreen mainScreen].bounds.size.width - 16;
|
||||
self.messageLabel.frame = CGRectMake(0, 8, w - 16, 36);
|
||||
self.preferredContentSize = CGSizeMake(0, 52);
|
||||
}
|
||||
|
||||
- (void)showStops:(NSArray *)stops {
|
||||
for (UIView *v in self.view.subviews) if (v != self.messageLabel) [v removeFromSuperview];
|
||||
self.messageLabel.hidden = YES;
|
||||
self.stops = stops;
|
||||
|
||||
CGFloat w = self.view.bounds.size.width ?: [UIScreen mainScreen].bounds.size.width - 16;
|
||||
CGFloat y = 4;
|
||||
NSDate *now = [NSDate date];
|
||||
for (NSUInteger si = 0; si < stops.count; si++) {
|
||||
NSDictionary *s = stops[si];
|
||||
UILabel *head = [[UILabel alloc] initWithFrame:CGRectMake(0, y, w - 16, kHeadH)];
|
||||
head.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
|
||||
head.textColor = [UIColor colorWithWhite:1 alpha:0.6];
|
||||
head.text = [[TodayViewController shortName:s[@"name"]] uppercaseString];
|
||||
head.tag = 1000 + si; // Antippen -> Haltestelle in der App
|
||||
[self.view addSubview:head];
|
||||
y += kHeadH;
|
||||
|
||||
NSInteger shown = 0;
|
||||
for (NSDictionary *d in s[@"departures"] ?: @[]) {
|
||||
if (shown >= kPerStop) break;
|
||||
if (![d isKindOfClass:[NSDictionary class]] || [NN(d[@"cancelled"]) boolValue]) continue;
|
||||
NSDate *when = [TodayViewController dateFromISO:NN(d[@"when"])] ?: [TodayViewController dateFromISO:NN(d[@"plannedWhen"])];
|
||||
NSInteger mins = when ? (NSInteger)floor([when timeIntervalSinceDate:now] / 60.0) : -1;
|
||||
if (when && mins < 0) continue;
|
||||
NSInteger delay = [NN(d[@"delay"]) integerValue] / 60;
|
||||
|
||||
UIImageView *badge = [[UIImageView alloc] initWithImage:[TodayViewController badgeForLine:NN(d[@"line"])]];
|
||||
badge.frame = CGRectMake(0, y + (kRowH - 19) / 2, badge.image.size.width, 19);
|
||||
[self.view addSubview:badge];
|
||||
|
||||
UILabel *dir = [[UILabel alloc] initWithFrame:CGRectMake(48, y, w - 48 - 16 - 62, kRowH)];
|
||||
dir.font = [UIFont systemFontOfSize:14];
|
||||
dir.textColor = [UIColor whiteColor];
|
||||
dir.text = [TodayViewController shortName:NN(d[@"direction"]) ?: @""];
|
||||
[self.view addSubview:dir];
|
||||
|
||||
UILabel *min = [[UILabel alloc] initWithFrame:CGRectMake(w - 16 - 60, y, 60, kRowH)];
|
||||
min.font = [UIFont monospacedDigitSystemFontOfSize:15 weight:UIFontWeightSemibold];
|
||||
min.textAlignment = NSTextAlignmentRight;
|
||||
min.textColor = delay > 0 ? [UIColor colorWithRed:1 green:0.45 blue:0.4 alpha:1] : [UIColor whiteColor];
|
||||
min.text = !when ? @"–" : (mins <= 0 ? @"jetzt" : [NSString stringWithFormat:@"%ld min", (long)mins]);
|
||||
[self.view addSubview:min];
|
||||
|
||||
y += kRowH;
|
||||
shown++;
|
||||
}
|
||||
if (!shown) {
|
||||
UILabel *none = [[UILabel alloc] initWithFrame:CGRectMake(0, y, w - 16, kRowH)];
|
||||
none.font = [UIFont systemFontOfSize:13];
|
||||
none.textColor = [UIColor colorWithWhite:1 alpha:0.6];
|
||||
none.text = @"Keine Abfahrten in der nächsten Stunde";
|
||||
[self.view addSubview:none];
|
||||
y += kRowH;
|
||||
}
|
||||
y += 6;
|
||||
}
|
||||
self.preferredContentSize = CGSizeMake(0, y + 2);
|
||||
}
|
||||
|
||||
- (void)tapped:(UITapGestureRecognizer *)tap {
|
||||
// welche Haltestelle wurde angetippt? (Bereich ab ihrer Ueberschrift)
|
||||
CGPoint p = [tap locationInView:self.view];
|
||||
NSDictionary *hit = nil;
|
||||
for (UIView *v in self.view.subviews) {
|
||||
if (v.tag >= 1000 && v.frame.origin.y <= p.y && (NSUInteger)(v.tag - 1000) < self.stops.count) hit = self.stops[v.tag - 1000];
|
||||
}
|
||||
NSString *urlString = hit
|
||||
? [NSString stringWithFormat:@"bvgnearby://stop?id=%@&name=%@",
|
||||
[TodayViewController encode:[hit[@"id"] description]], [TodayViewController encode:hit[@"name"] ?: @""]]
|
||||
: @"bvgnearby://";
|
||||
[self.extensionContext openURL:[NSURL URLWithString:urlString] completionHandler:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.serverlele.BVGNearby</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.serverlele.BVGNearby</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,16 +1,52 @@
|
||||
#!/bin/bash
|
||||
# Baut BVGNearby + Widget, signiert mit ldid (AppSync) und installiert aufs iPhone.
|
||||
# ./deploy.sh normal
|
||||
# NO_WIDGET=1 ./deploy.sh ohne Widget/App Group (falls das Widget Probleme macht)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
python3 BVGNearby/check_methods.py BVGNearby
|
||||
|
||||
# 1) App bauen
|
||||
xcodebuild -project BVGNearby.xcodeproj -scheme BVGNearby -configuration Debug \
|
||||
-destination 'generic/platform=iOS' -derivedDataPath build \
|
||||
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO INFOPLIST_KEY_UIMainStoryboardFile= \
|
||||
-quiet build
|
||||
|
||||
APP=build/Build/Products/Debug-iphoneos/BVGNearby.app
|
||||
PB=/usr/libexec/PlistBuddy
|
||||
plutil -remove UIMainStoryboardFile "$APP/Info.plist" 2>/dev/null || true
|
||||
ldid -S "$APP/BVGNearby"
|
||||
|
||||
# URL-Schema bvgnearby:// (Widget oeffnet damit Haltestellen in der App)
|
||||
$PB -c "Delete :CFBundleURLTypes" "$APP/Info.plist" 2>/dev/null || true
|
||||
$PB -c "Add :CFBundleURLTypes array" \
|
||||
-c "Add :CFBundleURLTypes:0 dict" \
|
||||
-c "Add :CFBundleURLTypes:0:CFBundleURLName string com.serverlele.BVGNearby" \
|
||||
-c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" \
|
||||
-c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string bvgnearby" "$APP/Info.plist"
|
||||
|
||||
# 2) Widget bauen (ohne Xcode-Target, direkt mit clang)
|
||||
rm -rf "$APP/PlugIns"
|
||||
if [ -z "$NO_WIDGET" ]; then
|
||||
APPEX="$APP/PlugIns/BVGWidget.appex"
|
||||
mkdir -p "$APPEX"
|
||||
xcrun -sdk iphoneos clang -arch armv7 -arch arm64 -miphoneos-version-min=9.0 \
|
||||
-fobjc-arc -fmodules -fapplication-extension -e _NSExtensionMain \
|
||||
-framework Foundation -framework UIKit -framework NotificationCenter -framework CoreLocation \
|
||||
Widget/TodayViewController.m -o "$APPEX/BVGWidget"
|
||||
cp Widget/Info.plist "$APPEX/Info.plist"
|
||||
# Versionsnummern muessen zur App passen
|
||||
V=$($PB -c "Print CFBundleShortVersionString" "$APP/Info.plist")
|
||||
B=$($PB -c "Print CFBundleVersion" "$APP/Info.plist")
|
||||
$PB -c "Set :CFBundleShortVersionString $V" -c "Set :CFBundleVersion $B" "$APPEX/Info.plist"
|
||||
plutil -convert binary1 "$APPEX/Info.plist"
|
||||
ldid -SWidget/widget.entitlements "$APPEX/BVGWidget"
|
||||
ldid -SWidget/app.entitlements "$APP/BVGNearby"
|
||||
else
|
||||
ldid -S "$APP/BVGNearby"
|
||||
fi
|
||||
|
||||
# 3) Paket bauen und installieren
|
||||
rm -rf build/Payload build/BVGNearby.ipa
|
||||
mkdir build/Payload && cp -r "$APP" build/Payload/
|
||||
(cd build && zip -qr BVGNearby.ipa Payload)
|
||||
|
||||
Reference in new issue
Block a user