Skip to content

1.0.0

Latest

Choose a tag to compare

@Luminoid Luminoid released this 04 Oct 23:09
· 2 commits to main since this release

[1.0.0] - 2026-10-04

LumiKit 1.0 rebuilds the package as a public UI framework: five products, one naming scheme, a theme value that every component follows live, a Style on every component, built-in strings in four languages, and the iOS 26 APIs adopted with fallbacks for iOS 18. Every 0.x app needs the migration: read docs/MIGRATION-1.0.md, then run Scripts/migrate-1.0.sh <app-dir> --dry-run.

Breaking changes

Products

Old New Why
LumiKitNetwork LumiKitDebug The network logger and its inspector screens ship in one DEBUG-only product
Photo browser, grid, crop editor, pick-and-crop coordinator, and share preview in LumiKitUI LumiKitPhoto Apps that never show a photo no longer link PhotosUI
LumiKitUI depended on LumiKitNetwork LumiKitUI depends on LumiKitCore and SnapKit only Link LumiKitDebug yourself, under #if DEBUG
LMKLottieRefreshControl.animationBundle defaulted to .main and needed an app-supplied JSON The package bundles its ring animation, tinted from the theme Delete the app's copy, or pass animation: for a custom one

Naming

One naming scheme, documented in CONTRIBUTING.md. The migration script renames all of these.

Old New
LMKConcurrencyHelpers, LMKDateHelper, LMKDateFormatterHelper, LMKFormatHelper, LMKFileUtil LMKConcurrency, LMKDate, LMKDateFormat, LMKFormat, LMKFile
LMKAnimationHelper, LMKHapticFeedbackHelper, LMKDeviceHelper, LMKSceneUtil LMKAnimation, LMKHaptics, LMKDevice, LMKScene
LMKImageUtil, LMKDominantColorExtractor, LMKQRCodeGenerator LMKImage (symbol, downsample, dominantColor, qrCode); encodeJPEG(_:maxDimension:quality:) is encodeJPEG(_:maxPixelSize:quality:) and caps in pixels, like downsample
LMKAlertPresenter, LMKCountdownConfirmation LMKAlert
LMKShareService, LMKPhotoEXIFService, LMKDatePickerHelper, LMKOverscrollFooterHelper LMKShare, LMKPhotoMetadata, LMKDatePicker, LMKOverscrollFooterView
LMKBottomSheetController, LMKCardPageController, LMKCardPanelController, LMKSegmentedPageController the same names ending in ViewController
LMKEnumSelectionBottomSheet LMKEnumPicker
LMKToastType, LMKBannerType LMKStatus
LMKChipStyle, LMKEmptyStateStyle, LMKGradientDirection, LMKDividerOrientation, LMKTipStyle, LMKTipArrowDirection, LMKTextFieldState LMKChipView.Variant, LMKEmptyStateView.Layout, LMKGradientView.Direction, LMKDividerView.Orientation, LMKTipView.Placement, LMKTipView.ArrowDirection, LMKValidationState
LMKGlassView.Style, LMKProgressViewController.Style, LMKSegmentedControl.CornerStyle, LMKActionSheet.ActionStyle .Variant, .Mode, .Corners, Action.Style (Style now names each component's style struct)
LMKDeviceType, LMKScreenSize, LMKButtonRole, LMKTypographyType, LMKShadowConfig LMKDevice.Kind, LMKDevice.ScreenSize, LMKButton.Role, LMKTypography.Kind, LMKShadowTheme.Shadow
LMKPhotoGridContentMode, LMKPhotoGridSortOrder LMKPhotoGridViewController.ContentMode, .SortOrder
LMKRequestData, LMKResponseData LMKNetworkRequestRecord.Request, .Response
LMK*Strings types and lmk*Strings globals nested Type.Strings, with Type.strings for the whole app and an instance strings on types you create
Collection[safe:], String?.nonEmpty, NSAttributedString + [lmk_safe:], .lmk_nonEmpty, lmk_appending(_:)
UIControl.lmk_touchAreaEdgeInsets, lmk_pointInside lmk_hitTestInsets, lmk_point(inside:with:) (for your own controls; LumiKit controls keep a 44pt target themselves)
URLSessionConfiguration.enableNetworkLogging() lmk_enableNetworkLogging()
LMKFileUtil.generateTempFileURL(fileExtension:), clearTmpDirectory() LMKFile.temporaryURL(extension:) (non-optional), clearTemporaryFiles(olderThan:matchingPrefix:)

Theme

  • The LMKTheme protocol, LMKDefaultTheme, and LMKThemeManager are gone. Colors are an LMKColorTheme struct with a fully defaulted initializer, and LMKTheme is the whole theme as one value (colors, typography, spacing, cornerRadius, shadow, alpha, layout, animation, extensions) with static current, apply(_:), update(_:), reset(), observe(_:), and updates. An app theme is extension LMKTheme { static let myApp = LMKTheme(colors: LMKColorTheme(primary: ...)) }, applied with LMKTheme.apply(.myApp). configure(colors:typography:...) and the per-category apply(spacing:) are gone.
  • Applying a theme re-renders every window: LMKColor.* values are dynamic colors, and components restyle themselves. Token reads (LMKSpacing.large, LMKColor.primary, ...) work from any isolation, so static let defaults and SwiftUI views can use them.
  • Color roles: white to onAccent, black to scrim, primaryDark to primaryVariant, imageBorder to outline, graySoft to fillStrong, grayMuted to fill; photoBrowserBackground to LMKPhotoBrowserViewController.Style.backgroundColor. New roles: link, pressedOverlay, selection, and highContrastBoost (applied to accents under Increase Contrast).
  • LMKAlpha is a seven-step ramp with the same values: overlayLight to xxs, overlayMedium to xs, overlayDark to small, semiTransparent to medium, overlay to large, overlayStrong to xl, overlayOpaque to xxl, dimmingOverlay to dimming.
  • LMKShadow.small() / button() / cellCard() / card() / medium() / large() became LMKShadow.style(for: .level1 ... .level5); LMKShadow.opacity became iconOverlayOpacity.
  • LMKAnimation.Duration has seven steps: buttonPress to instant; uiShort and photoLoad to fast; actionSheet and alert to normal; modalPresentation, listUpdate, listInsertDelete, and cardExpand to moderate; screenTransition and errorShake to slow; successFeedback to emphasis. Spring and Curve are value types on LMKAnimation.
  • The factories are gone: LMKButtonFactory.filled(role:title:) became LMKButton(title:style: .filled(.role)), LMKLabelFactory.caption(text:) became UILabel.lmk_make(.caption, text:), LMKCardFactory.cardView() became LMKCardView(style: .cell). LMKButton.Style presets take an LMKButton.Role, not a color (.filled(.destructive)); .tint(_:) sets a custom color.
  • The layout constants (LMKBottomSheetLayout, LMKCardPageLayout, LMKCardPanelLayout, LMKTipLayout, LMKFloatingButtonLayout, LMKPhotoBrowserConfig, LMKBadgeTheme with LMKBadge) and the overridable appearance properties on the base controllers moved into each component's Style; the search bar metrics in LMKLayout live on LMKSearchBar.Style.

Presentation

  • Anything that presents from a view controller takes from:: LMKAlert, LMKErrorHandler, LMKActionSheet, LMKEnumPicker, LMKDatePicker, LMKShare, the photo browser, sheets, and panels. Views shown inside a hierarchy use show(in:): toasts, tips, banners, the floating button. Every dismissal is dismiss(), and every completion closure is completion:.
  • LMKToast: the ten showSuccess / showSuccessOnWindow entry points became LMKToast.show(_ status:_ message:duration:in:completion:), show(LMKToast.Configuration), and dismissAll(in:). With no host, a toast shows on the active window, above presented sheets.
  • LMKBottomSheetViewController.addAsChild(sheet, in:) and LMKCardPanelViewController.show(panel, in:) became the instance method present(from:); dismissSheet() became dismiss(reason:completion:) and dismissPanel(completion:) became dismiss(completion:), both safe to call twice; init(cancelTitle:) became init(style:) with the title on strings. animateIn() and animateOut(velocity:completion:) are no longer public. UIKit's dismiss(animated:completion:) on a sheet or panel runs the component's own dismissal.
  • LMKActionSheet is a namespace: build an LMKActionSheet.Configuration and call LMKActionSheet.present(_:from:); the view controller is LMKActionSheetViewController. Custom content sizes itself (contentHeight: is gone).
  • LMKEnumPicker.present(from:title:options:selection:onSelect:onCancel:) handles single (T?) and multiple (Set<T>) selection; presentMultiSelect is gone. LMKEnumSelectable.iconName is String? and defaults to nil.
  • LMKDatePicker.present(_ configuration:from:onConfirm:) with Configuration, .past(...), and .future(...), plus presentRange, presentCalendarRange, and presentWithTextField, replace the seven presenters; every form takes an onCancel. makePicker(_:) builds a picker whose style works under the Mac idiom.
  • LMKAlert.presentTextInput(TextInput, from:onSave:onCancel:), presentActionSheet(actions: [LMKAlert.Action], from:anchor:onCancel:), presentDeleteConfirmation, presentCountdownConfirmation, and confirm(...) async -> Bool replace the parameter-heavy forms; LMKErrorHandler.present(from:). confirm and LMKErrorHandler.confirmRetry return false when the alert cannot be shown or goes away without an action.
  • LMKFloatingButton has no shared instance: keep the button returned by show(icon:in:positionKey:onTap:) and call dismiss() on it.
  • LMKShare.present(_ items:from:anchor:completion:) shares images, files, text, and links in one sheet. file(at:) keeps the file unless you pass deletesAfterShare: true (0.x shareFile always deleted it).

Callbacks

  • Closures are named on<Event> and carry the new value: tapHandler to onTap, actionHandler to onAction, dismissHandler to onDismiss, valueChangedHandler and stateChangedHandler to onValueChange, textChangedHandler to onTextChange, pageChangedHandler to onPageChange, selectionChangedHandler and multiSelectionChangedHandler to onSelectionChange (Set<Int>), backAction to onBack. LMKButton.didTapHandler is gone. Two payloads changed: LMKTextField / LMKTextView.onTextChange carries String (0.x carried String?), and LMKCheckboxCell.onValueChange carries the new isDone (0.x onToggle carried nothing). Every presenter that can be cancelled offers onCancel.
  • Single-method delegates became closures: LMKSearchBarDelegate (onTextChange, onSearch, onBeginEditing, onEndEditing, onCancel), LMKPhotoCropDelegate (onCrop, onCancel), LMKSharePreviewDelegate (onShare, onSave, onFailure). The photo browser and grid data sources and delegates stay.
  • LMKBottomSheetViewController.onDismissTapped() became onDismiss: ((DismissReason) -> Void)? with cancelButton, dimmingTap, drag, keyCommand, and programmatic reasons.

Localization

  • Every user-visible default comes from the package's string tables (English, Spanish, Simplified Chinese, Traditional Chinese) through nested Strings structs. Apps that localized LumiKit's strings themselves can delete those overrides; apps in other languages set Type.strings once at launch, or instance.strings per view.

Logging

  • LMKLogLevel has six cases, debug, info, notice, warning, error, and fault, so an exhaustive switch needs the two new ones. warning now logs at the unified log's error type (it was default), and notice at default, the lowest type the device keeps.
  • Debug lines are no longer compiled out of Release builds. minimumLevel filters at runtime: .debug by default in DEBUG builds, .info otherwise. A threshold above .error is treated as .error, so errors and faults are always written.
  • An attached error is logged as a public summary with its domain and code (failed [NSURLErrorDomain -1001 <- NSPOSIXErrorDomain 60]) plus its full description as private detail, in place of | Error: <localizedDescription>.
  • LMKLogging.log gained a privateDetail: String? parameter; a custom conformer adds it.
  • LumiKit's own lines moved from .general, .ui, .data, .network, and .error to the .lumiKit category.

Removed

Removed Use instead
LMKToggleButton LMKButton with isToggle = true, selectedTitle / selectedImage, onValueChange
LMKPhotoGridViewController as the browser's LMKPhotoBrowserDataSource / LMKPhotoBrowserDelegate (numberOfPhotos, photo(at:), photoDate(at:), and the other display-index members) Ask your own data source (display order follows sortOrder), and reach the presented browser through grid.browser
LMKKeyboardInsetHelper; LMKKeyboardObserver is internal scrollView.lmk_enableKeyboardAdjustment() / lmk_disableKeyboardAdjustment()
UIView.lmk_fadeIn / lmk_fadeOut LMKAnimation.fadeIn(_:) / fadeOut(_:)
lmk_safeAreaSnp, lmk_setEdgesEqualToSuperview, lmk_centerInSuperview, lmk_setAutoLayoutSize plain SnapKit
UITableView.lmk_configureCellHighlight, UIButton.lmk_animatePress cell.lmk_configureCustomHighlight(), LMKAnimation.animateButtonPress(_:)
lmk_configureIconListRow, lmk_emptyStateCell cell.lmk_applyListRow(LMKListRowConfiguration(...)); host an LMKEmptyStateView yourself
LMKPhotoEXIFService.extractDate(from: UIImage) and the other UIImage overloads LMKPhotoMetadata.read(from:) on Data, a file URL, a PHPickerResult, or an NSItemProvider (a decoded image has no metadata left to read)
LMKDateFormatterHelper.dateFormatter(...), configure(dateFormat:) LMKDateFormat.string(_:date:time:), LMKDateFormat.preferredDatePattern
LMKNavigationBar.setLeftItemEnabled(at:) / setRightItemEnabled(at:), pinToTop(of:) updateItem(_:_:) by identifier, install(in:)
LMKBottomSheetViewController.containerBottomConstraint, refreshSheetColors() additionalBottomInset, applyTheme(_:)
LMKConcurrency.encode / decode returning optionals the same names throw; try? keeps the optional
LMKPhotoBrowserCell, LMKCountdownConfirmationViewController, LMKNetworkDetailViewController, LMKNetworkRequestStore, LMKNavigationDirection no longer public

Added

Theming and styling

  • A slot on LMKTheme for every component (theme.button, theme.chip, theme.navigationBar, theme.toast, theme.monthCalendar, theme.photoBrowser, theme.lottieRefreshControl, and the rest), LMKThemeExtension for app-defined values, LMKThemeApplying with lmk_startApplyingTheme(), and traitCollection.lmkTheme / traitOverrides.lmkTheme, so a window or subtree can preview a theme without changing the app's.
  • A Style on every component, every field optional (nil means the theme decides), with merging(_:), public structural subviews, and a didApplyStyle hook that runs last. Shared pieces: LMKSurfaceStyle (background clear, solid, gradient, blur, or glass; corners square, fixed, capsule, circle, or concentric; solid, dashed, or inset borders; shadows; content insets), LMKControlStateStyle for highlighted, selected, disabled, and focused looks, and UIView.lmk_apply(surface:defaults:). Turn a part off explicitly with .square, LMKBorderStyle.hidden, or LMKShadowSource.hidden. The open base classes (LMKButton, LMKBottomSheetViewController, LMKCardPageViewController, LMKCardPanelViewController, LMKScrollStackViewController, LMKSegmentedPageViewController, LMKTabBarController) call applyContentTheme(_:), where a subclass styles its own content. Components that play haptics have a haptics style switch.
  • Shadows and borders set with lmk_applyShadow(_:) and lmk_applyBorder(...) follow theme, Dark Mode, and contrast changes.
  • Typography: LMKTextStyle (h1 to h4, body, caption, and small steps, custom(LMKFontSpec)), lmk_apply(_:color:) on labels, text fields, and text views, UILabel.lmk_make(_:text:color:numberOfLines:), LMKTypographyTheme.maximumScale, and fontDesign / headingFontDesign for SF Rounded, Serif, or Mono on every step or only the headings. Fixed heights became Dynamic Type minimums.
  • Tokens: LMKLayout.symbolMicro through symbolHero, rowHeightCompact / rowHeight / rowHeightComfortable / rowHeightEstimated, readableContentMaxWidth; LMKColor.onFill(_:preferred:) and resolved(_:with:); LMKShadow.Level; LMKAnimation.Spring, Curve, and pressScale.

Controls

  • LMKButton: Role by Variant (filled, tinted, outlined, ghost, glass, icon-only) by Size; highlighted, selected, disabled, and focused looks; title, image, setSymbol(_:), isToggle, isLoading, menu with showsMenuIndicator, shrinkingTitleToFit(minimumScaleFactor:), minimumHitTarget, Style.animatesSymbolChanges (iOS 26), and init(title:style:target:action:).
  • New controls: LMKCheckbox, LMKRatingControl, LMKCopyableLabel, LMKPhotoButton, and LMKActionTile (with UIColor.lmk_glyphTint(onLightAccentDarkenBy:)). LMKActionTile.Style.glyphMinimumContrast keeps the glyph readable against the tile in every appearance, titleMinimumScaleFactor shrinks a long title, and minimumHeight sets a floor; at accessibility text sizes a long press shows the tile's title and glyph. LMKPhotoButton.onDropImage accepts an image dragged onto it on iPad and the Mac.
  • LMKSegmentedControl.Layout (equalWidth, fitContent, scrollable(padding:spacing:)), setItems, insertSegment / removeSegment, and setEnabled(_:forSegmentAt:).
  • LMKSlider ticks and neutralValue (iOS 26), Style.disabled, and a VoiceOver value. LMKTextField and LMKTextView share LMKTextInputStyle (with disabled and per-LMKValidationState looks), pass every delegate call on to your delegate, report onBeginEditing / onEndEditing, and count maxCharacterCount in characters; the text field has a themable clearButton, and the text view grows between minimumHeight and maximumHeight. LMKSearchBar gained cancelButtonMode, debounceInterval with onDebouncedTextChange, vertical contentInsets, and a public textField.
  • Every LumiKit control keeps a 44pt touch target while enabled; a disabled control absorbs touches inside its bounds, as UIKit's controls do.

Components

  • LMKStatus (success, warning, error, info, neutral), shared by toasts, banners, LMKStatusLabel, and validation. Toasts: LMKToast.Configuration (title, action, a duration in seconds or persistent, position, presentation, queuePolicy), LMKToast.Handle (dismiss, setMessage), and LMKToast.showUndo with a countdown ring, an optional status or glyph, and a position; only Undo or the countdown ends it unless tapToDismiss is on, so a tap that misses Undo commits nothing. LMKAlert.TextInput.additionalActions adds buttons after Save (such as a destructive Remove), and a secure text input turns off autocorrection and the other text suggestions.
  • LMKBannerView.show(in:below:insetting:insetsScrollView:): a banner over a screen sits under its navigation bar, caps its width at the readable width, and pushes the scroll view's content down by its height; isFloating, plus Style.horizontalMargin, verticalMargin, maxWidth, and dismissSymbolPointSize.
  • Tips: LMKTip with LMKTipView.Placement; a pointed tip's Style.surface colors, borders (solid or dashed), and shadows wrap the bubble and its arrow together, and Style.arrowColor fills the arrow under a gradient, blur, or glass bubble. A tip is a VoiceOver modal that the escape gesture dismisses.
  • LMKSkeletonView (placeholder shapes), LMKGlassView(variant:) and LMKGlassContainerView, LMKEmptyStateView.Content with asContentUnavailableConfiguration() and configure(_:animated:), LMKFilterChipBar.SelectionMode, LMKCardView presets (cell, elevated, flat, outlined) and onTap, angled and radial LMKGradientView gradients, an LMKOverscrollFooterView that follows its scroll view, LMKChipView as a UIControl with a selected look and Variant.tinted, LMKPageIndicator touch targets and right-to-left layout, and LMKToastView.Style.dismissButton.
  • LMKChipFlowView (theme.chipFlow): chips, or any views, in lines that wrap at the view's width, with Style.spacing and lineSpacing; its height follows its width, so a self-sizing cell around it resizes.
  • Lists: LMKListRowConfiguration (symbol, image, or async image leading; title, subtitle, and detail; disclosure, checkmark, switch, or badge trailing), LMKListRowContentView with highlighted and selected looks, lmk_applyListRow(_:), lmk_installRowPointerInteraction(_:), UIListContentConfiguration.lmk_applyTextStyle / lmk_applyLeadingSymbol / lmk_applyThumbnail, and LMKListTable.makeInsetGrouped().
  • LMKMonthCalendarView with subclassable LMKCalendarDayCell and LMKCalendarDayDecoration (dots, badges, glyphs), single, range, and multiple selection, swipe paging that respects Reduce Motion, a today mark that follows the device's date and moves at midnight, and day numerals in the locale's digits. Core adds LMKCalendarDay, LMKCalendarMonth, LMKCalendarSelection, and LMKCalendarSelectionMode: Gregorian dates whatever calendar you supply, with day and month arithmetic that daylight-saving changes cannot shift.
  • Detail cards: LMKDetailCard (a header, typed rows: key-value, text, chips, photo strip, progress, navigation, link, rating, image, divider, custom; and actions), LMKDetailCardView (setValue(_:forRowID:), update(rowID:_:)), and LMKDetailPageViewController (cards updated by id, Edit and Share items, Command-E for onEdit, beginEditing with Command-Return and Escape).
  • LMKMenu: option menus as native UIMenus built from sections (single choice, multiple toggles that flip while the menu stays open, sort with a direction, actions, submenu, custom), rebuilt from your state each time the menu opens, and anchored to a bar button, a navigation bar item, or an LMKButton; reloadVisibleMenu(presenting:) repaints an open menu. LMKSortMenu adds a sort menu with a live direction arrow, a layout section, and additionalSections for filters and commands.
  • LMKTabBarController, LMKTab (lazy roots, a search role), and LMKTabBarAppearance: selectTab(identifier:), reorderTabs, badges, Command-1 to Command-9, the iPad sidebar, applyContentTheme(_:), and the iOS 26 minimize behavior and bottom accessory.

Navigation and containers

  • LMKNavigationBar.Style.appearance (automatic, classic, glass): by default the bar matches the running OS, with Liquid Glass item capsules on iOS 26 and tinted items over a hairline before; Style.itemGlass and itemGlassViews style the capsules. LMKNavigationBarItem has identifier, isEnabled, menu, badge, and role (plain, prominent, destructive); the bar adds updateItem(_:_:), install(in:), subtitle, backgroundContentView, and pinScrollView(_:edgeEffect:). Under the Mac idiom, glyph items and the back button show their label as a tooltip. For system bars: makeBarButtonItem(), UINavigationItem.lmk_setItems(leading:trailing:), and lmk_setSubtitle(_:).
  • LMKNavigationController with LMKPopGestureConfiguring: the swipe back follows canBeginPopGesture, also with the system bar hidden and with the iOS 26 content-area swipe; the top screen sets the status bar style.
  • LMKBottomSheetViewController: drags that hand off to an inner scroll view, Escape and Command-W, contentLayoutGuide, resolveStyle(for:) for subclasses, and VoiceOver modal behavior. LMKActionSheet.Configuration, RowStyle.disabled, and a public LMKActionSheetRowView; LMKEnumPicker search and disabled options, sized to its rows; LMKDatePicker.Configuration; LMKCalendarRangeSelectionView.Style and locale.
  • LMKCardPageViewController: leadingItem / trailingItem (titled items, roles, badges), usesSystemNavigationBar (pushed onto a stack whose bar shows, the page hands its title and items to that bar), and Style.showsDragIndicator for a page in a sheet. LMKCardPanelViewController.presentation (overlay window or modal), Style.heightRatio, Escape and Command-W, VoiceOver modal behavior, and UIViewController.lmk_cardPanel.
  • LMKScrollStackViewController and LMKFormScaffold width modes (inset, readable, capped), makeFieldRow, and makeHeaderStack; LMKSegmentedPageViewController.segmentedControlPlacement and trackpad swipe paging; LMKProgressViewController finished states, observe(_ progress:), and Escape; LMKErrorHandler.policy.

Core and utilities

  • LMKDateFormat on Date.FormatStyle: Context (locale, calendar, time zone, hour cycle), string(_:date:time:), intervalString, rangeLabel, residenceLabel, relativeDayString, clockTime, dateWithClockTime, usesTwelveHourClock, widestClockSample, monthYearString, weekdaySymbols, formatter(pattern:), and preferredDatePattern for a user-chosen date format. LMKFormat number and percent formatting.
  • LMKLogger: notice and fault; a runtime minimumLevel; private: on every level for user data (the message stays public unless messagePrivacy says otherwise); error: on every level; describe(_:); once(_:_:_:) and resetOnce(_:) for repeating failures; record(_:) for entries another package already wrote; entryHandler, log(_:_:), and the LMKLogging protocol with LMKLogger.default; the .lumiKit category; LMKLogLevel is Comparable with osLogType; LMKLogEntry is Codable and carries privateDetail, file, function, and line; LMKLogStore.formatted() includes the private detail for on-device debugging.
  • LMKErrorHandler.present / confirmRetry and LMKConcurrency.executeTask log your call site. Image encoding and downsampling, photo metadata, the crop editor, the pick-and-crop coordinator, and presenting a share sheet or picker log a warning when they fail.
  • LMKURLValidator.validate(_:) returns Result<URL, ValidationError> with the reason; the blocklist covers more private, reserved, and loopback addresses and host spellings; normalizeBaseURL(_:preservingPathExtension:).
  • LMKFile.temporaryURL(extension:), clearTemporaryFiles(olderThan:matchingPrefix:), and clearTemporaryFilesInBackground(olderThan:matchingPrefix:), both returning a count; LMKConcurrency.encode / decode throw and accept custom coders; onMainActor / onMainActorAfter return their Task; String.lmk_trimmedOrNil; LMKDate.initialize() is optional.
  • LMKImage.downsample(data:maxPixelSize:options:) and downsample(fileURL:...) (sync and async, with HDR), pixelSize, downsampledJPEG, imageSize, and SymbolOptions (palette, hierarchical, and multicolor rendering, variable value, and the iOS 26 modes). LMKPhotoMetadata reads from Data, a URL, a PHPickerResult, or an NSItemProvider, and writes a date and coordinate.
  • LMKScene.activeWindowScene, keyWindow, presentingViewController, requestClose(_:onError:), configureMacWindow(for:minimumSize:maximumSize:hidesTitleBar:), and observeGeometry(of:onChange:) (size, safe areas, orientation, and the iOS 26 interactive-resize flag); LMKDevice.observeScreenSize(of:onChange:); LMKTheme.observe(_:) returns a token that ends the observation when released.
  • UIViewController.lmk_formKeyCommands(save:cancel:) with an overridable lmk_cancelFromKeyCommand(), UIView.lmk_pinReadableWidth(in:) and lmk_readableWidthGuide, lmk_displayScale, lmk_forceLayoutDirection(_:), UIScrollView.lmk_disableKeyboardAdjustment(), the UISplitViewController.lmk_setInspector(_:) family, and lmk_apply(_:animatingDifferences:in:) for diffable data sources.
  • LMKMarkdownRenderer.render and renderFull can be called off the main actor.
  • WCAG contrast helpers on UIColor: lmk_relativeLuminance(resolvedWith:), lmk_contrastRatio(to:resolvedWith:), and lmk_softestTone(over:washAlpha:minimumContrast:resolvedWith:), the softest tone of a color that keeps a contrast ratio against a wash of itself.
  • LMKShare.Item (image, file, text, url) with text / url conveniences; LMKHaptics.isEnabled; UIColor.lmk_composited(over:alpha:) for an opaque tinted surface and lmk_stateShade(by:) for pressed and selected fills; LMKLayout.pixelAligned(_:scale:) and pixelAligned(_:for:) for lines of even thickness.

Photo

  • Photo browser: Style with theme.photoBrowser, a zoom transition from zoomSourceView, HDR display, an orientation lock on iOS 26, onDismiss, key commands, and stageView. Gestures: a double tap zooms on the tapped point, a zoomed photo pans to its edges and pages past them, a long press plays a Live Photo, and a trackpad pinch on the Mac zooms around the pointer.
  • Grid: allowsMultipleSelection with selectedIndices and onSelectionChange (a sideways drag selects a range and scrolls near the edges), contextMenuProvider, prefetch hooks (photoGridPrefetch, photoGridCancelPrefetch), photoGridThumbnail(at:pixelSize:) for thumbnails sized to the cell, browser for the presented browser, drag and drop with other apps (allowsDraggingPhotos, which carries the photo's original file when the data source implements photoGridFileURL(at:), and onDropImages, both off by default), a pinch that changes the column count around the fingers (Style.pinchFeedback), a floating glass toolbar, and the top scroll-edge effect. LMKSinglePhotoViewer.browser and dismiss(completion:).
  • Crop editor: aspectRatios and initialAspectRatio (LMKCropAspectRatio gains 16:9 and 9:16), onCrop / onCancel, keyboard shortcuts, and a crop frame VoiceOver can move and resize.
  • Pick-and-crop coordinator: save(image, metadata) async, onPicked, onCancel, onFailure (Failure is a LocalizedError; with no handler, failures show through LMKErrorHandler), Strings, and maximumPixelSize.
  • Share preview: detents, onShare / onSave / onFailure / onDismiss, and isSaving; Save Image shows only when the app declares NSPhotoLibraryAddUsageDescription, unless Style.showsSaveButton says otherwise.
  • LMKPhotoMetadata.write keeps the image data intact (every frame and gain map) and writes EXIF dates with their time-zone offset; read honors them.

Debug

  • LMKNetworkLogger.Configuration: credential redaction on by default for the common auth, cookie, and API-key headers (redactedHeaderFields) and query items (redactedQueryItems), applied to the URL, a Location header, and a URL password; hostFilter, body caps, and recordsDidChangeNotification; redact(_:configuration:) and record(id:).
  • Request bodies are captured (isBodyTruncated says when the cap cut one), logged requests keep the app's cookies, cache, and credentials, and redirects go back to the app's session, which decides whether to follow them; both hops are recorded. While logging is on, a refused redirect completes about half a second later.
  • LMKNetworkRequestRecord.outcome (pending, success, redirect, error) and isRedirect, next to isSuccess and isError. LMKNetworkHistoryViewController lists records with an icon per outcome and an empty state, and its localized detail screen copies to a pasteboard entry that expires. The whole product compiles only under LMK_ENABLE_NETWORK_LOGGING.

Lottie

  • The bundled ring, tinted from theme.lottieRefreshControl; Style (pullThreshold, timeline, minimumSpinDuration, size, tint); Timeline(animation:) for your own animation (split at a PHASE2_SPIN_LOOP marker, or looped whole); install(on:style:onRefresh:); and makeRefreshKeyCommand(action:) for Command-R. The ring follows appearance, contrast, and Reduce Motion changes.

Platform

  • iOS 26 APIs with fallbacks for iOS 18: Liquid Glass surfaces and glass buttons, concentric corners, scroll-edge effects, tab bar minimize behavior and bottom accessory, UISearchTab, navigation subtitles, prominent bar items with badges, slider ticks and neutral value, symbol transitions and rendering modes, the split view inspector column, HDR headroom, right-to-left natural alignment, interactive resizing, orientation lock, and background extension views. docs/PLATFORM.md lists each one and the iOS 27 follow-ups.
  • Mac Catalyst: every component works under the Mac idiom; there the Lottie refresh control returns nil from install and offers Command-R instead.

Documentation and tooling

  • DocC documentation for all five products, docs/MIGRATION-1.0.md, docs/PLATFORM.md, CONTRIBUTING.md, and SECURITY.md.
  • Scripts/migrate-1.0.sh renames types, members, and call shapes in your app and reports, by file and line, the edits to make by hand; --dry-run shows the changes without writing them.
  • An Example app with a page for every component, a catalog search, a theme switcher, and a scripted accessibility check (truncation, clipping, overlap, touch targets, labels, contrast, and fixed fonts) across every page.

Changed

Defaults an app upgrading from 0.12 will see differently.

  • LMKDevice.screenSize classifies by the window's portrait width and size classes instead of the screen height: up to 375pt is .compact, up to 402pt .regular, wider .large, and only a window regular in both size classes is .extraLarge. iPhone mini, X, and 11 Pro now read .compact, iPhone 14 Pro through 17 Pro read .regular, and an iPad in Slide Over or a narrow split is no longer .extraLarge. LMKSpacing.cardPadding and cellPaddingVertical follow: iPads step by window size instead of all getting the large values, and narrow windows get phone spacing.
  • On iOS 26, LMKNavigationBar draws its items on Liquid Glass with no hairline, like the system bar. theme.navigationBar.appearance = .classic keeps the 0.12 look.
  • LMKButton: every button now shrinks slightly and plays a light haptic when pressed (0.12 did this only for buttons given a style); Style.pressAnimation and Style.haptics turn them off. A button that opens its menu on tap does not shrink. While isLoading is on, taps no longer fall through to the views behind it, VoiceOver still reads the title, and an icon-only button keeps its width. Under the Mac idiom, buttons keep their iOS look instead of drawing as bare titles.
  • Chips: a selected filled LMKChipView shows a darker fill instead of switching to an outline (Style.selectedVariant = .outlined keeps the 0.12 swap), and a tappable chip shows a pressed look. An LMKFilterChipBar with a filled chip style draws unselected chips in a soft tint and the selected chip fully filled.
  • LMKBannerView has an opaque tinted background with a thin border in the status color, larger corners, and a smaller dismiss glyph, in place of 0.12's translucent tint.
  • LMKPhotoBrowserViewController and LMKPhotoCropViewController present full screen on their own (0.12 showed them as a sheet when presented directly). The browser presents over the screen it came from, so a dismiss drag reveals that screen; set modalPresentationStyle = .fullScreen for a black backdrop. A dismiss drag moves the photo with the finger at full size and fades only the background, where 0.12 shrank the photo and faded the whole browser.
  • In a regular-width window (iPad, Mac), LMKBottomSheetViewController and the action sheet and enum picker built on it are capped at Style.maxWidth (the readable width by default) and centered, instead of spanning the whole window.
  • LMKTipView keeps its bubble inside the safe area, and .automatic placement picks above or below by the room on each side. VoiceOver reads the tap-anywhere area as "Got it, button".
  • LMKLoadingStateView stays hidden until startLoading(); an inline view without a set height sizes to its content.
  • LMKBadgeView keeps its own size in stack views. lmk_makeCircular() keeps a view round as its size changes. Views inside LMKGradientView are reachable by VoiceOver.
  • LMKSwitch's thumb uses LMKColor.onAccent instead of pure white; Style.thumbTint = .white keeps the 0.12 thumb.
  • With no value given, LMKColorTheme.primaryVariant is an opaque, slightly darker shade of primary (0.12's primaryDark default was translucent green), and outline is divider at half opacity. Pass both to LMKColorTheme(...) to keep your 0.12 colors.
  • Small text (.small, .extraSmall) and the helper text and character counter of LMKTextField and LMKTextView default to LMKColor.textSecondary instead of textTertiary, to meet WCAG AA contrast.
  • Labels made with UILabel.lmk_make align natural text to the interface's layout direction, like a plain UILabel; 0.12's factory labels followed the text's own direction.
  • UIColor(lmk_hex:) returns nil for a string with any non-hex character; 0.12 parsed the valid leading part.
  • LMKLogEntry.message holds only the logged text, with the call site in file, function, and line; formattedMessage gives the 0.12 string, and LMKLogStore.formatted() still includes the call site.
  • LMKNetworkRequestRecord.isError no longer counts a 3xx response: 304 Not Modified is isSuccess, other 3xx responses are isRedirect, and a record with a transport error is isError and never isSuccess. displayDuration follows the locale ("2,500ms").

Fixed

Bugs present in 0.12.

Controls and text input

  • LMKSwitch and LMKSegmentedControl ignored isEnabled: a disabled control looked the same and still changed value. LMKSwitch.setOn(_:animated:) never animated.
  • A vertical scroll that started on an LMKSegmentedControl was taken by its indicator drag, and taps near its edges did nothing. A segmented control sized to its titles could collapse a segment at large text sizes.
  • LMKSlider crashed in Mac Catalyst apps that use the Mac idiom.
  • LMKSlider, LMKSearchBar, the LMKCardPageViewController header buttons, and the LMKSharePreviewViewController close button had touch targets smaller than 44pt. LMKPageIndicator had no VoiceOver label.
  • A display-only LMKChipView swallowed taps meant for the view behind it, such as a table cell. Outlined chip borders could look thicker on some edges than on others on 3x screens.
  • LMKTextField and LMKTextView counted maxCharacterCount in UTF-16 units, so an emoji used up several characters; they blocked input-method composition near the limit and rejected an over-long paste instead of keeping what fit. LMKTextField's clear button never appeared, and LMKTextView helper text could wrap one word per line.

Sheets, panels, and navigation

  • Bottom sheets: a tall sheet lifted by the keyboard could rise above the top safe area, a sheet presented while the keyboard was up sat behind it, and a dismissed sheet could not be presented again. Rows in a multi-page LMKActionSheet stayed tappable during the page slide, so a tap could run an action from the wrong page. LMKEnumPicker rows had a fixed height and clipped at large text sizes. LMKDatePicker.presentRange leaked its pickers.
  • LMKCardPanelViewController ignored safe areas, did not give key-window status back to the window underneath after an overlay panel closed, and dropped touches meant for a controller the panel presented.
  • LMKCardPageViewController drew its header under the status bar, or under the Mac window controls, when the page filled the screen, and setting title after the view loaded did not update the header.
  • LMKNavigationBar ignored the left and right safe-area insets, so in landscape its items could sit under the sensor housing.
  • LMKScrollStackViewController and LMKFormScaffold measured their content from the screen edges, so on iOS 26 content slid under a floating tab bar or split view sidebar, and in landscape under the sensor housing.
  • LMKSegmentedPageViewController: tapping a segment during a page slide moved the control without changing the page, the page swipe fired alongside sliders and the indicator drag, setPage(_:animated:) before the view loaded was ignored, and the first page was laid out at zero width. Under the Mac idiom the segmented control did not show; it now sits in the window toolbar.
  • Under the Mac idiom, an LMKNavigationController with a visible bar lost its toolbar back button after a full-screen presentation over it closed, and after a presented split view closed its back button and title sat a sidebar's width in from the window controls.
  • lmk_topViewController recursed until it crashed on an empty navigation controller, and lmk_presentAlertOnTop re-centered an action sheet popover that was already anchored to a view.

Feedback and status

  • A banner shown over a screen covered the content without making room for it and let the content show through, and with Reduce Motion on a dismissed banner shown again stayed invisible.
  • A pointed tip stayed where it first appeared after a rotation or resize, and dismiss() could call onDismiss twice.
  • LMKFloatingButton kept its old position when the window rotated or resized, and a drag could leave it under a side safe-area inset.
  • LMKEmptyStateView could show its message one word per line when first laid out at zero width, as a table background is. LMKLoadingStateView logged a broken constraint as a table background.
  • LMKAlert.presentCountdownConfirmation with a long message grew taller than the screen and pushed its buttons out of reach.
  • In right-to-left layouts an overflowing LMKFilterChipBar opened on its last chips. LMKPageIndicator crashed on a negative page count. The skeleton shimmer stopped for good after the app went to the background.

Photo

  • Photo browser: a zoomed photo could be panned past its edges, and a double tap zoomed on a different point than the one tapped. A dismiss drag faded the photo along with the background. A swipe made while the browser was opening was undone, and the browser jumped back to its first photo whenever it reappeared. Rotating or resizing left the pages misaligned. At 1x, the first sideways drag on a narrow photo moved the photo instead of paging. Releasing a pinch past the maximum zoom brought the controls back over the photo, and the LIVE badge stayed when the other controls hid. In right-to-left layouts the pages and arrow keys ran the wrong way, and on the Mac paging while zoomed left the controls hidden.
  • Photo grid cells held full-size images, which used a lot of memory.
  • Crop editor: it made a full-size copy of the photo before cropping, could lose a locked aspect ratio, and took a pinch only with both fingers outside the crop frame. Pick-and-crop could fail to show the crop editor when the photo loaded before the picker finished closing.
  • Share preview: tapping Save twice saved twice; Save in an app without NSPhotoLibraryAddUsageDescription went to the photo library anyway (which terminates the app) instead of reporting .photoLibraryAccessDenied; and a failed write that came back without an error was neither logged nor reported.

Core and utilities

  • Reading LMKDevice.deviceType, isIPad, or isMacCatalyst off the main thread crashed, and LMKDevice.hasTopNotch returned true on iPads.
  • LMKDate.calendar and the helpers built on it (today, isToday, startOfDay) kept the launch time zone after a time-zone change unless the app had called LMKDate.initialize().
  • LMKURLValidator accepted loopback and private addresses written in shorthand, octal, or hex IPv4 form (127.1, 0177.0.0.1), and *.localhost hosts.
  • UIScrollView.lmk_enableKeyboardAdjustment() scrolled a focused text view that was itself the adjusted scroll view, and left extra space above the keyboard.
  • LMKMarkdownRenderer could style the wrong line as a heading, and missed tables in text with Windows (CRLF) line endings.
  • Under Reduce Motion, LMKAnimation.animateErrorShake removed the view's own border.
  • A cell using lmk_applyCustomHighlight(highlighted:animated:) could lose its highlight when pressed again during the un-highlight.
  • LMKShadowStyle had no public initializer.
  • Calling LMKLogger.configure(subsystem:) while other threads were logging was a data race.

Debug and Lottie

  • With network logging on, request bodies were never recorded, redirects were followed without the app's session seeing them, and logged requests dropped the app's cookies, cache, and credentials. Sessions set up with lmk_enableNetworkLogging() kept being intercepted after LMKNetworkLogger.disable().
  • LMKLottieRefreshControl started a refresh after the pull went back below the threshold, or on a bounce or a programmatic scroll, and showed a still ring when UIKit started the refresh before the finger lifted.

Deferred

Codable themes, a LumiKitSwiftUI product, video in the photo browser, and the iOS 27 APIs (listed in docs/PLATFORM.md) are 1.x work.