Summary
You dealt with the problem of switching a bank under running
code by making sure that the next instruction address in the other bank had
the appropriate code to continue executing your program. This was set up by
setting the system to read from one bank but write to the other so that
your executing code could run from one bank but set up the other bank.
The following three sections discuss the original Apple II Language Card
banking; the new main/alternate Extended 80 Column Card added to the IIe
and IIc, and give a more detailed description of how to run your own code
in the alternate bank.
Apple II ROM/RAM Banking
...Apple IIe auxiliary RAM bank select register address -- C073 or C07X?
the Apple IIe had a very simple scheme with a single variable to select
which 64K of RAM was currently active.
The scheme was not so simple. The main/auxiliary bank select allowed access
to the RAM supplied by the Extended 80-column Text Card, but that
was new to the IIe (as an official Apple API; it was actually first
implemented in 3rd party cards), and was in addition to the much older
Apple II/II+ Language Card banking scheme for the top 12K of the address
space.
That pre-IIe system offered ROM or RAM for $E000-$FFFF (mapping 8K of 16K)
and ROM or one of two RAM banks for $D000-$DFFF (mapping 4K of 12K). (The
$C800-$CFFF range could also be mapped on the fly to ROM in any of the
expansion slots, but that's probably not so relevant to this discussion.)
The upper 8K bank and one of the lower 4K banks were often used for a
different version of the ROM (e.g., to use the Integer BASIC ROM code on an
Apple II+ with Applesoft ROMs) but of course if you were using the standard
on-board ROM code (or not using ROM code at all) all 16K of RAM in those
three banks was available for any purpose you liked.
A very useful additional feature was that the bank selection could be set
to let you read ROM but write RAM. So if you needed to write new code and
data to RAM in the $D000-$FFFF range for later use you could do this while
still using the $D000-$FFFF ROM bank and switch only after the RAM was
fully set up. (You could not, however, read one RAM bank and write the
other RAM bank in the $D000-$DFFF range.)
Apple IIe Additions
The additional bank switching added by the Apple IIe with the extended 80
column RAM card was the alternate bank, which was essentially a full
duplication of all the RAM banks above. That is, as well as two RAM banks
for $0000-$BFFF (the lower 48K) you now had ROM and two 8K RAM banks at
$E000-$FFFF and ROM and four 4K RAM banks at $D000-$DFFF. The IIc also
supported a second 12K alternate ROM bank, though this was used only from
the second version (version 0—the first version was version 255 or -1) of
the ROMs onward. I don't know offhand if the IIe supported the same.
(The IIc provided the card slot ROMs at $C800-$CFFF for all slots from
onboard ROM as well, and this also was expanded with alternate mapping for
these with the ROM expansion. This worked exactly as everything else did
above; the ROM for a particular slot was chosen with the usual Apple II
scheme to do that and whether the alternate bank was selected determined
which of the two banks for that slot was selected.)
Additionally, and unlike the $D000-$DFFF RAM banks, for the $0000-$BFFF
range it was possible to set the read and write banks separately, so that
you could run code and read data from the main bank but write data to the
auxiliary bank or vice versa.
The Apple IIc Technical Reference Manual describes all this in detail in
Chapter 2: Memory Organization and Control section "Overview of the
Address Space" (p.20). (This is described similarly in other IIe and
IIc manuals; this particular one is just what I happened to have at hand.)
Here is the overview diagram:
Using Code in the Alternate Bank
Say your program is running in bank 0 and wants to swap in bank 1 to
stash some data. So you execute an instruction to switch banks... and the
next instruction fails to execute, because now bank 1 is active, so
that's where the CPU is getting code from, but it's initially empty, so
your program promptly crashes before it has any opportunity to copy code
across.
Presumably the machine must have provided some way to get around this
problem.
The way to get around this was to make sure that, if you were switching an
area of memory where you were currently executing code, to make sure that
you had first set up that area to have the correct to continue program
execution in the other bank.
Generally, you'd run setup code from the main bank in the lower 48K, switch
the write (but not read) bank to the alternate bank, and write all your
additional code to the alternate bank before ever trying to execute from
it. You were responsible for making sure that at any instruction where you
switched read from main to alternate bank the code in alternate bank at the
next location was the correct code to continue executing (and vice versa
when switching from the alternate to the main bank).
The later IIc ROMs did this themselves, too; the code makes extensive use
of switching between main and alternate ROM banks. The ROM also provided routines for copying data
between RAM banks (as mentioned in Peterl's answer) so your own code didn't have to handle the switching.